Repository: a-h/templ (go)

Issue:
performance: templ parse takes a long time and uses high CPU when unclosed void elements are used
**Describe the bug**
With the code I show here templ takes VERY long to compile, about 29 seconds. If I add more cases to the switch case it takes even longer, to the point where it is completey unusable and locked.

If I just remove the inputs in the top of the ActionStep template it goes below one second again...

This seems like a really funkey scanner issue to me, but I am sure you are much faster in finding the actual issue... 

the templ process also climbs to ridiculous CPU usage while trying to generate in this case...

**To Reproduce**
Repo: https://github.com/s00500/templ-switchcaseissue

**Expected behavior**
Fast compile, no lockup

**Logs**
```
❯ templ generate
(!) templ version check: failed to read go.mod file: open /go.mod: no such file or directory
(✓) Complete [ updates=1 duration=28.554246583s ]
````

**`templ info` output**
```
❯ templ info
(✓) os [ goos=darwin goarch=arm64 ]
(✓) go [ location=/usr/local/go/bin/go version=go version go1.22.1 darwin/arm64 ]
(✓) gopls [ location=/Users/lb/go/bin/gopls version=golang.org/x/tools/gopls v0.16.1 ]
(✓) templ [ location=/Users/lb/go/bin/templ version=v0.2.759 ]

```
**Desktop (please complete the following information):**
macOS (latest, sonoma 14.6.1)

❯ templ version
v0.2.759

(Masterbuild from today)

![templ](https://github.com/user-attachments/assets/7bfc3c8e-be87-458b-a1a6-04b27b5210b1)




Pull request description:
refactor: improve void element parsing perf, fixes #886 and #857
This PR changes the behaviour of templ's void element handling.

Previously, you could do `<input>Text</input>` and templ would allow it, despite it not being valid HTML. The LSP would emit a warning, but it would render.

However, this caused a performance issue for unclosed void elements. templ was parsing the rest of the file to look for a closing element, just in case it was present, so that the elements could be nested in templ's object model.

This operation wasn't memoized, so it ran every time a void element was encountered. In files with lots of void elements, it would cause a long delay!

The formatting operation rewrites `<br>` to `<br/>` which vastly reduces the time, but it's not a great experience that it takes a long time. It seems that some users don't use the editor extensions, or are unaware of the fmt tool, so they just live with the known bad performance.

This PR changes the templ behaviour so that it doesn't have nested content for all known void HTML elements, e.g.:

```html
<input>Text</input>
```

Becomes:

```html
<input/>Text
```

This improves the performance, see benchmark results:

## Before

```
pkg: github.com/a-h/templ/parser/v2
BenchmarkParse-16           3538            387117 ns/op          194075 B/op       4684 allocs/op
BenchmarkFormat-16          3138            400745 ns/op          197759 B/op       4765 allocs/op
```

## After

```
pkg: github.com/a-h/templ/parser/v2
BenchmarkParse-16          23718             46576 ns/op           23478 B/op        544 allocs/op
BenchmarkFormat-16         19960             60805 ns/op           26874 B/op        625 allocs/op
```

## Comparison

The important part is the ns/op figure - how many nanoseconds each operation takes.

46576 / 387117=12%, so the new version is about 8 times faster in the given scenario.

Patch:
diff --git a/.version b/.version
index 1148158..24cb2fb 100644
--- a/.version
+++ b/.version
@@ -1,1 +1,1 @@
-0.2.763
\ No newline at end of file
+0.2.766
\ No newline at end of file
diff --git a/parser/v2/diagnostics.go b/parser/v2/diagnostics.go
index a8ee5e9..8f36653 100644
--- a/parser/v2/diagnostics.go
+++ b/parser/v2/diagnostics.go
@@ -2,7 +2,6 @@ package parser
 
 import (
 	"errors"
-	"fmt"
 )
 
 type diagnoser func(Node) ([]Diagnostic, error)
@@ -35,7 +34,6 @@ func walkNodes(t []Node, f func(Node) bool) {
 
 var diagnosers = []diagnoser{
 	useOfLegacyCallSyntaxDiagnoser,
-	voidElementWithChildrenDiagnoser,
 }
 
 func Diagnose(t TemplateFile) ([]Diagnostic, error) {
@@ -64,20 +62,3 @@ func useOfLegacyCallSyntaxDiagnoser(n Node) ([]Diagnostic, error) {
 	}
 	return nil, nil
 }
-
-func voidElementWithChildrenDiagnoser(n Node) (d []Diagnostic, err error) {
-	e, ok := n.(Element)
-	if !ok {
-		return
-	}
-	if !e.IsVoidElement() {
-		return
-	}
-	if len(e.Children) == 0 {
-		return
-	}
-	return []Diagnostic{{
-		Message: fmt.Sprintf("void element <%s> should not have child content", e.Name),
-		Range:   e.NameRange,
-	}}, nil
-}
diff --git a/parser/v2/elementparser.go b/parser/v2/elementparser.go
index ae71d5b..3c3e73d 100644
--- a/parser/v2/elementparser.go
+++ b/parser/v2/elementparser.go
@@ -375,6 +375,34 @@ var (
 	})
 )
 
+// Void element closer.
+var voidElementCloser voidElementCloserParser
+
+type voidElementCloserParser struct{}
+
+var voidElementCloseTags = []string{"</area>", "</base>", "</br>", "</col>", "</command>", "</embed>", "</hr>", "</img>", "</input>", "</keygen>", "</link>", "</meta>", "</param>", "</source>", "</track>", "</wbr>"}
+
+func (voidElementCloserParser) Parse(pi *parse.Input) (n Node, ok bool, err error) {
+	var ve string
+	for _, ve = range voidElementCloseTags {
+		s, canPeekLen := pi.Peek(len(ve))
+		if !canPeekLen {
+			continue
+		}
+		if !strings.EqualFold(s, ve) {
+			continue
+		}
+		// Found a match.
+		ok = true
+		break
+	}
+	if !ok {
+		return nil, false, nil
+	}
+	pi.Take(len(ve))
+	return nil, true, nil
+}
+
 // Element.
 var element elementParser
 
@@ -396,28 +424,19 @@ func (elementParser) Parse(pi *parse.Input) (n Node, ok bool, err error) {
 
 	// Once we've got an open tag, the rest must be present.
 	l := pi.Position().Line
-	endOfOpenTag := pi.Index()
 
 	// If the element is self-closing, even if it's not really a void element (br, hr etc.), we can return early.
-	if ot.Void {
+	if ot.Void || r.IsVoidElement() {
 		// Escape early, no need to try to parse children for self-closing elements.
 		return addTrailingSpaceAndValidate(start, r, pi)
 	}
 
-	// Void elements _might_ have children, even though it's invalid.
-	// We want to allow this to be parsed.
+	// Parse children.
 	closer := StripType(parse.All(parse.String("</"), parse.String(ot.Name), parse.Rune('>')))
 	tnp := newTemplateNodeParser(closer, fmt.Sprintf("<%s>: close tag", ot.Name))
 	nodes, _, err := tnp.Parse(pi)
 	if err != nil {
 		notFoundErr, isNotFoundError := err.(UntilNotFoundError)
-		if r.IsVoidElement() && isNotFoundError {
-			// Void elements shouldn't have children, or a close tag, so this is expected.
-			// When the template is reformatted, we won't reach here, because <hr> will be converted to <hr/>.
-			// Return the element as we have it.
-			pi.Seek(endOfOpenTag)
-			return addTrailingSpaceAndValidate(start, r, pi)
-		}
 		if isNotFoundError {
 			err = notFoundErr.ParseError
 		}
@@ -443,6 +462,10 @@ func (elementParser) Parse(pi *parse.Input) (n Node, ok bool, err error) {
 }
 
 func addTrailingSpaceAndValidate(start parse.Position, e Element, pi *parse.Input) (n Node, ok bool, err error) {
+	// Elide any void close tags.
+	if _, _, err = voidElementCloser.Parse(pi); err != nil {
+		return e, false, err
+	}
 	// Add trailing space.
 	ws, _, err := parse.Whitespace.Parse(pi)
 	if err != nil {
diff --git a/parser/v2/templateparser.go b/parser/v2/templateparser.go
index a553eca..1965637 100644
--- a/parser/v2/templateparser.go
+++ b/parser/v2/templateparser.go
@@ -61,6 +61,10 @@ type templateNodeParser[TUntil any] struct {
 
 var rawElements = parse.Any(styleElement, scriptElement)
 
+var templateNodeSkipParsers = []parse.Parser[Node]{
+	voidElementCloser, // </br>, </img> etc. - should be ignored.
+}
+
 var templateNodeParsers = []parse.Parser[Node]{
 	docType,                // <!DOCTYPE html>
 	htmlComment,            // <!--
@@ -80,6 +84,7 @@ var templateNodeParsers = []parse.Parser[Node]{
 }
 
 func (p templateNodeParser[T]) Parse(pi *parse.Input) (op Nodes, ok bool, err error) {
+outer:
 	for {
 		// Check if we've reached the end.
 		if p.until != nil {
@@ -94,6 +99,17 @@ func (p templateNodeParser[T]) Parse(pi *parse.Input) (op Nodes, ok bool, err er
 			}
 		}
 
+		// Skip any nodes that we don't care about.
+		for _, p := range templateNodeSkipParsers {
+			_, matched, err := p.Parse(pi)
+			if err != nil {
+				return Nodes{}, false, err
+			}
+			if matched {
+				continue outer
+			}
+		}
+
 		// Attempt to parse a node.
 		// Loop through the parsers and try to parse a node.
 		var matched bool
diff --git a/parser/v2/textparser.go b/parser/v2/textparser.go
index 12a9d2b..d5353a9 100644
--- a/parser/v2/textparser.go
+++ b/parser/v2/textparser.go
@@ -25,6 +25,11 @@ var textParser = parse.Func(func(pi *parse.Input) (n Node, ok bool, err error) {
 	}
 	t.Range = NewRange(from, pi.Position())
 
+	// Elide any void element closing tags.
+	if _, _, err = voidElementCloser.Parse(pi); err != nil {
+		return
+	}
+
 	// Parse trailing whitespace.
 	ws, _, err := parse.Whitespace.Parse(pi)
 	if err != nil {


Test patch:
diff --git a/generator/test-html/template.templ b/generator/test-html/template.templ
index 8afe58f..4f338ca 100644
--- a/generator/test-html/template.templ
+++ b/generator/test-html/template.templ
@@ -10,5 +10,5 @@ templ render(p person) {
 	<hr noshade?={ true }/>
 	<hr optionA optionB?={ true } optionC="other" optionD?={ false }/>
 	<hr noshade/>
-	<input name="test">Text</input>
+	<input name="test"/>Text
 }
diff --git a/generator/test-html/template_templ.go b/generator/test-html/template_templ.go
index 59b15ad..4143b56 100644
--- a/generator/test-html/template_templ.go
+++ b/generator/test-html/template_templ.go
@@ -103,7 +103,7 @@ func render(p person) templ.Component {
 				return templ_7745c5c3_Err
 			}
 		}
-		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("><hr noshade><input name=\"test\">Text</input>")
+		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("><hr noshade><input name=\"test\">Text")
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
diff --git a/parser/v2/benchmarks_test.go b/parser/v2/benchmarks_test.go
new file mode 100644
index 0000000..1ac7b28
--- /dev/null
+++ b/parser/v2/benchmarks_test.go
@@ -0,0 +1,34 @@
+package parser
+
+import (
+	_ "embed"
+	"strings"
+	"testing"
+)
+
+//go:embed benchmarktestdata/benchmark.txt
+var benchmarkTemplate string
+
+func BenchmarkParse(b *testing.B) {
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		if _, err := ParseString(benchmarkTemplate); err != nil {
+			b.Fatal(err)
+		}
+	}
+}
+
+func BenchmarkFormat(b *testing.B) {
+	b.ReportAllocs()
+	sb := new(strings.Builder)
+	for i := 0; i < b.N; i++ {
+		tf, err := ParseString(benchmarkTemplate)
+		if err != nil {
+			b.Fatal(err)
+		}
+		if err = tf.Write(sb); err != nil {
+			b.Fatal(err)
+		}
+		sb.Reset()
+	}
+}
diff --git a/parser/v2/benchmarktestdata/benchmark.txt b/parser/v2/benchmarktestdata/benchmark.txt
new file mode 100644
index 0000000..e7c7ffc
--- /dev/null
+++ b/parser/v2/benchmarktestdata/benchmark.txt
@@ -0,0 +1,18 @@
+package benchmarktestdata
+
+templ Benchmark() {
+				<div>Hello</div>
+				<br>
+				<br>
+				<br>
+				</br>
+				<div>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</div>
+				<br>
+				<br>
+				<ul>
+				<li>Item 1</li>
+				<li>Item 2</li>
+				<li>Item 3</li>
+				</ul>
+}
+
diff --git a/parser/v2/diagnostics_test.go b/parser/v2/diagnostics_test.go
index 3a7cea5..e0a7b45 100644
--- a/parser/v2/diagnostics_test.go
+++ b/parser/v2/diagnostics_test.go
@@ -134,21 +134,6 @@ templ template () {
 }`,
 			want: nil,
 		},
-		{
-			name: "voidElementWithChildrenDiagnoser: with diagnostics",
-			template: `
-package main
-
-templ template () {
-	<div>
-	  <input>Child content</input>
-	</div>
-}`,
-			want: []Diagnostic{{
-				Message: "void element <input> should not have child content",
-				Range:   Range{Position{46, 5, 4}, Position{51, 5, 9}},
-			}},
-		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/parser/v2/elementparser_test.go b/parser/v2/elementparser_test.go
index 7761670..0c7796d 100644
--- a/parser/v2/elementparser_test.go
+++ b/parser/v2/elementparser_test.go
@@ -550,6 +550,20 @@ if test {
 	}
 }
 
+func TestVoidElementCloserParser(t *testing.T) {
+	t.Run("all void elements are parsed", func(t *testing.T) {
+		for _, input := range voidElementCloseTags {
+			_, ok, err := voidElementCloser.Parse(parse.NewInput(input))
+			if err != nil {
+				t.Fatalf("unexpected error: %v", err)
+			}
+			if !ok {
+				t.Fatalf("failed to parse %q", input)
+			}
+		}
+	})
+}
+
 func TestElementParser(t *testing.T) {
 	tests := []struct {
 		name     string
@@ -662,15 +676,9 @@ func TestElementParser(t *testing.T) {
 					From: Position{Index: 1, Line: 0, Col: 1},
 					To:   Position{Index: 6, Line: 0, Col: 6},
 				},
-				Children: []Node{
-					Text{
-						Value: "Text",
-						Range: Range{
-							From: Position{Index: 7, Line: 0, Col: 7},
-							To:   Position{Index: 11, Line: 0, Col: 11},
-						},
-					},
-				},
+				// <input> is a void element, so text is not a child of the input.
+				// </input> is ignored.
+				Children: nil,
 			},
 		},
 		{
@@ -736,7 +744,7 @@ func TestElementParser(t *testing.T) {
 			},
 		},
 		{
-			name:  "element: void nesting others is OK (br/hr)",
+			name:  "element: void nesting is ignored",
 			input: `<br><hr></br>`,
 			expected: Element{
 				Name: "br",
@@ -744,15 +752,9 @@ func TestElementParser(t *testing.T) {
 					From: Position{Index: 1, Line: 0, Col: 1},
 					To:   Position{Index: 3, Line: 0, Col: 3},
 				},
-				Children: []Node{
-					Element{
-						Name: "hr",
-						NameRange: Range{
-							From: Position{Index: 5, Line: 0, Col: 5},
-							To:   Position{Index: 7, Line: 0, Col: 7},
-						},
-					},
-				},
+				// <br> is a void element, so <hr> is not a child of the <br>.
+				// </br> is ignored.
+				Children: nil,
 			},
 		},
 		{
@@ -1549,11 +1551,19 @@ amount is charged</div>`,
 				},
 				Children: []Node{
 					Text{
-						Value:         "for which any ",
+						Value: "for which any ",
+						Range: Range{
+							From: Position{Index: 5, Line: 0, Col: 5},
+							To:   Position{Index: 19, Line: 0, Col: 19},
+						},
 						TrailingSpace: SpaceVertical,
 					},
 					Text{
-						Value:         "amount is charged",
+						Value: "amount is charged",
+						Range: Range{
+							From: Position{Index: 20, Line: 1, Col: 0},
+							To:   Position{Index: 37, Line: 1, Col: 17},
+						},
 						TrailingSpace: SpaceNone,
 					},
 				},
diff --git a/parser/v2/formattestdata/void_elements_are_converted_to_self_closing_elements.txt b/parser/v2/formattestdata/void_elements_are_converted_to_self_closing_elements.txt
index 16cf47d..92caeb2 100644
--- a/parser/v2/formattestdata/void_elements_are_converted_to_self_closing_elements.txt
+++ b/parser/v2/formattestdata/void_elements_are_converted_to_self_closing_elements.txt
@@ -20,6 +20,7 @@ templ input(value, validation string) {
 	<img></img>
 	<input>
 	<input></input>
+	<input>Text
 	<input>Text</input>
 	<keygen>
 	<keygen></keygen>
@@ -59,7 +60,8 @@ templ input(value, validation string) {
 	<img/>
 	<input/>
 	<input/>
-	<input>Text</input>
+	<input/>Text
+	<input/>Text
 	<keygen/>
 	<keygen/>
 	<link/>
diff --git a/parser/v2/templateparser_test.go b/parser/v2/templateparser_test.go
index 4bd287b..6b0eff2 100644
--- a/parser/v2/templateparser_test.go
+++ b/parser/v2/templateparser_test.go
@@ -806,6 +806,44 @@ func TestTemplateParser(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "template: void element closers are ignored",
+			input: `templ Name() {
+	<br></br><br>
+}`,
+			expected: HTMLTemplate{
+				Range: Range{
+					From: Position{Index: 0, Line: 0, Col: 0},
+					To:   Position{Index: 31, Line: 2, Col: 1},
+				},
+				Expression: Expression{
+					Value: "Name()",
+					Range: Range{
+						From: Position{Index: 6, Line: 0, Col: 6},
+						To:   Position{Index: 12, Line: 0, Col: 12},
+					},
+				},
+				Children: []Node{
+					Whitespace{Value: "\t"},
+					Element{
+						Name: "br",
+						NameRange: Range{
+							From: Position{Index: 17, Line: 1, Col: 2},
+							To:   Position{Index: 19, Line: 1, Col: 4},
+						},
+						TrailingSpace: SpaceNone,
+					},
+					Element{
+						Name: "br",
+						NameRange: Range{
+							From: Position{Index: 26, Line: 1, Col: 11},
+							To:   Position{Index: 28, Line: 1, Col: 13},
+						},
+						TrailingSpace: SpaceVertical,
+					},
+				},
+			},
+		},
 	}
 	for _, tt := range tests {
 		tt := tt