From 4dd5fc55948360fbc0d49566d6d7beaed809b7d5 Mon Sep 17 00:00:00 2001
From: Rich Harris <richard.a.harris@gmail.com>
Date: Mon, 14 Aug 2017 11:34:24 -0400
Subject: [PATCH] apply optimisation to raw tags

---
 src/generators/dom/visitors/RawMustacheTag.ts | 45 +++++++++++++------
 src/utils/CodeBuilder.ts                      |  8 ++--
 test/helpers.js                               |  2 +-
 .../expected-bundle.js                        |  4 +-
 .../each-block-changed-check/expected.js      |  4 +-
 .../helpers-invoked-if-changed/_config.js     | 38 ----------------
 .../_config.js                                | 26 +++++++++++
 .../counter.js                                |  3 +-
 .../main.html                                 |  9 +---
 .../ignore-unchanged-attribute/_config.js     | 26 +++++++++++
 .../ignore-unchanged-attribute/counter.js     |  3 ++
 .../ignore-unchanged-attribute/main.html      | 15 +++++++
 .../samples/ignore-unchanged-raw/_config.js   | 26 +++++++++++
 .../samples/ignore-unchanged-raw/counter.js   |  3 ++
 .../samples/ignore-unchanged-raw/main.html    | 15 +++++++
 .../samples/ignore-unchanged-tag/_config.js   | 26 +++++++++++
 .../samples/ignore-unchanged-tag/counter.js   |  3 ++
 .../samples/ignore-unchanged-tag/main.html    | 15 +++++++
 18 files changed, 203 insertions(+), 68 deletions(-)
 delete mode 100644 test/runtime/samples/helpers-invoked-if-changed/_config.js
 create mode 100644 test/runtime/samples/ignore-unchanged-attribute-compound/_config.js
 rename test/runtime/samples/{helpers-invoked-if-changed => ignore-unchanged-attribute-compound}/counter.js (53%)
 rename test/runtime/samples/{helpers-invoked-if-changed => ignore-unchanged-attribute-compound}/main.html (53%)
 create mode 100644 test/runtime/samples/ignore-unchanged-attribute/_config.js
 create mode 100644 test/runtime/samples/ignore-unchanged-attribute/counter.js
 create mode 100644 test/runtime/samples/ignore-unchanged-attribute/main.html
 create mode 100644 test/runtime/samples/ignore-unchanged-raw/_config.js
 create mode 100644 test/runtime/samples/ignore-unchanged-raw/counter.js
 create mode 100644 test/runtime/samples/ignore-unchanged-raw/main.html
 create mode 100644 test/runtime/samples/ignore-unchanged-tag/_config.js
 create mode 100644 test/runtime/samples/ignore-unchanged-tag/counter.js
 create mode 100644 test/runtime/samples/ignore-unchanged-tag/main.html

diff --git a/src/generators/dom/visitors/RawMustacheTag.ts b/src/generators/dom/visitors/RawMustacheTag.ts
index 41c3af9aa4..624e424983 100644
--- a/src/generators/dom/visitors/RawMustacheTag.ts
+++ b/src/generators/dom/visitors/RawMustacheTag.ts
@@ -12,12 +12,21 @@ export default function visitRawMustacheTag(
 ) {
 	const name = node._state.basename;
 	const before = node._state.name;
-	const value = block.getUniqueName(`${name}_value`);
 	const after = block.getUniqueName(`${name}_after`);
 
-	const { snippet } = block.contextualise(node.expression);
+	const { dependencies, indexes, snippet } = block.contextualise(node.expression);
 
-	block.addVariable(value);
+	const hasChangeableIndex = Array.from(indexes).some(index => block.changeableIndexes.get(index));
+
+	const shouldCache = (
+		node.expression.type !== 'Identifier' ||
+		block.contexts.has(node.expression.name) ||
+		hasChangeableIndex
+	);
+
+	const value = shouldCache && block.getUniqueName(`${name}_value`);
+	const init = shouldCache ? `${value} = ${snippet}` : snippet;
+	if (shouldCache) block.addVariable(value);
 
 	// we would have used comments here, but the `insertAdjacentHTML` api only
 	// exists for `Element`s.
@@ -38,17 +47,27 @@ export default function visitRawMustacheTag(
 
 	const isToplevel = !state.parentNode;
 
-	const mountStatement = `${before}.insertAdjacentHTML( 'afterend', ${value} = ${snippet} );`;
-	const detachStatement = `@detachBetween( ${before}, ${after} );`;
+	block.builders.mount.addLine(`${before}.insertAdjacentHTML( 'afterend', ${init} );`);
+	block.builders.detachRaw.addBlock(`@detachBetween( ${before}, ${after} );`);
+
+	if (dependencies.length || hasChangeableIndex) {
+		const changedCheck = (
+			( block.hasOutroMethod ? `#outroing || ` : '' ) +
+			dependencies.map(dependency => `'${dependency}' in changed`).join(' || ')
+		);
 
-	block.builders.mount.addLine(mountStatement);
+		const updateCachedValue = `${value} !== ( ${value} = ${snippet} )`;
 
-	block.builders.update.addBlock(deindent`
-		if ( ${value} !== ( ${value} = ${snippet} ) ) {
-			${detachStatement}
-			${mountStatement}
-		}
-	`);
+		const condition = shouldCache ?
+			( dependencies.length ? `( ${changedCheck} ) && ${updateCachedValue}` : updateCachedValue ) :
+			changedCheck;
 
-	block.builders.detachRaw.addBlock(detachStatement);
+		block.builders.update.addConditionalLine(
+			condition,
+			deindent`
+				@detachBetween( ${before}, ${after} );
+				${before}.insertAdjacentHTML( 'afterend', ${shouldCache ? value : snippet} );
+			`
+		);
+	}
 }
diff --git a/src/utils/CodeBuilder.ts b/src/utils/CodeBuilder.ts
index 934f29b9a4..2a412a8a06 100644
--- a/src/utils/CodeBuilder.ts
+++ b/src/utils/CodeBuilder.ts
@@ -21,15 +21,17 @@ export default class CodeBuilder {
 		this.lastCondition = null;
 	}
 
-	addConditionalLine(condition: string, line: string) {
+	addConditionalLine(condition: string, body: string) {
+		body = body.replace(/^/gm, '\t');
+
 		if (condition === this.lastCondition) {
-			this.result += `\n\t${line}`;
+			this.result += `\n${body}`;
 		} else {
 			if (this.lastCondition) {
 				this.result += `\n}`;
 			}
 
-			this.result += `${this.last === ChunkType.Block ? '\n\n' : '\n'}if ( ${condition} ) {\n\t${line}`;
+			this.result += `${this.last === ChunkType.Block ? '\n\n' : '\n'}if ( ${condition} ) {\n${body}`;
 			this.lastCondition = condition;
 		}
 
diff --git a/test/helpers.js b/test/helpers.js
index f4be292ef4..e3343d4aa8 100644
--- a/test/helpers.js
+++ b/test/helpers.js
@@ -125,7 +125,7 @@ export function normalizeHtml(window, html) {
 		.replace(/>[\s\r\n]+</g, '><')
 		.trim();
 	cleanChildren(node, '');
-	return node.innerHTML;
+	return node.innerHTML.replace(/<\/?noscript\/?>/g, '');
 }
 
 export function setupHtmlEqual() {
diff --git a/test/js/samples/each-block-changed-check/expected-bundle.js b/test/js/samples/each-block-changed-check/expected-bundle.js
index e0cb0b9ea4..b9f5b5b766 100644
--- a/test/js/samples/each-block-changed-check/expected-bundle.js
+++ b/test/js/samples/each-block-changed-check/expected-bundle.js
@@ -297,9 +297,9 @@ function create_each_block ( state, each_block_value, comment, i, component ) {
 				text_4.data = text_4_value;
 			}
 
-			if ( raw_value !== ( raw_value = comment.html ) ) {
+			if ( ( 'comments' in changed ) && raw_value !== ( raw_value = comment.html ) ) {
 				detachBetween( raw_before, raw_after );
-				raw_before.insertAdjacentHTML( 'afterend', raw_value = comment.html );
+				raw_before.insertAdjacentHTML( 'afterend', raw_value );
 			}
 		},
 
diff --git a/test/js/samples/each-block-changed-check/expected.js b/test/js/samples/each-block-changed-check/expected.js
index 48ac7c6613..18ba61d59a 100644
--- a/test/js/samples/each-block-changed-check/expected.js
+++ b/test/js/samples/each-block-changed-check/expected.js
@@ -123,9 +123,9 @@ function create_each_block ( state, each_block_value, comment, i, component ) {
 				text_4.data = text_4_value;
 			}
 
-			if ( raw_value !== ( raw_value = comment.html ) ) {
+			if ( ( 'comments' in changed ) && raw_value !== ( raw_value = comment.html ) ) {
 				detachBetween( raw_before, raw_after );
-				raw_before.insertAdjacentHTML( 'afterend', raw_value = comment.html );
+				raw_before.insertAdjacentHTML( 'afterend', raw_value );
 			}
 		},
 
diff --git a/test/runtime/samples/helpers-invoked-if-changed/_config.js b/test/runtime/samples/helpers-invoked-if-changed/_config.js
deleted file mode 100644
index fd8145ae74..0000000000
--- a/test/runtime/samples/helpers-invoked-if-changed/_config.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import counter from './counter.js';
-
-export default {
-	data: {
-		x: 1,
-		y: 2,
-		z: 3
-	},
-
-	html: `
-		<p>1</p>
-		<p class='2'>3</p>
-	`,
-
-	test(assert, component) {
-		counter.y = counter.z = 0;
-
-		component.set({ x: 4 });
-		assert.equal(counter.y, 0);
-		assert.equal(counter.z, 0);
-
-		component.set({ x: 5, y: 6 });
-		assert.equal(counter.y, 1);
-		assert.equal(counter.z, 0);
-
-		component.set({ x: 6, y: 6 });
-		assert.equal(counter.y, 1);
-		assert.equal(counter.z, 0);
-
-		component.set({ z: 7 });
-		assert.equal(counter.y, 1);
-		assert.equal(counter.z, 1);
-
-		component.set({ x: 8, z: 7 });
-		assert.equal(counter.y, 1);
-		assert.equal(counter.z, 1);
-	}
-};
diff --git a/test/runtime/samples/ignore-unchanged-attribute-compound/_config.js b/test/runtime/samples/ignore-unchanged-attribute-compound/_config.js
new file mode 100644
index 0000000000..5780a83e6c
--- /dev/null
+++ b/test/runtime/samples/ignore-unchanged-attribute-compound/_config.js
@@ -0,0 +1,26 @@
+import counter from './counter.js';
+
+export default {
+	data: {
+		x: 1,
+		y: 2
+	},
+
+	html: `
+		<p>1</p>
+		<p class='-2-'></p>
+	`,
+
+	test(assert, component) {
+		counter.count = 0;
+
+		component.set({ x: 3 });
+		assert.equal(counter.count, 0);
+
+		component.set({ x: 4, y: 5 });
+		assert.equal(counter.count, 1);
+
+		component.set({ x: 5, y: 5 });
+		assert.equal(counter.count, 1);
+	}
+};
diff --git a/test/runtime/samples/helpers-invoked-if-changed/counter.js b/test/runtime/samples/ignore-unchanged-attribute-compound/counter.js
similarity index 53%
rename from test/runtime/samples/helpers-invoked-if-changed/counter.js
rename to test/runtime/samples/ignore-unchanged-attribute-compound/counter.js
index 5367ba3c2b..63872cd6a2 100644
--- a/test/runtime/samples/helpers-invoked-if-changed/counter.js
+++ b/test/runtime/samples/ignore-unchanged-attribute-compound/counter.js
@@ -1,4 +1,3 @@
 export default {
-	y: 0,
-	z: 0
+	count: 0
 };
\ No newline at end of file
diff --git a/test/runtime/samples/helpers-invoked-if-changed/main.html b/test/runtime/samples/ignore-unchanged-attribute-compound/main.html
similarity index 53%
rename from test/runtime/samples/helpers-invoked-if-changed/main.html
rename to test/runtime/samples/ignore-unchanged-attribute-compound/main.html
index 1bcd70c4c1..1e742befd9 100644
--- a/test/runtime/samples/helpers-invoked-if-changed/main.html
+++ b/test/runtime/samples/ignore-unchanged-attribute-compound/main.html
@@ -1,18 +1,13 @@
 <p>{{x}}</p>
-<p class='{{getClass(y)}}'>{{myHelper(z)}}</p>
+<p class='-{{myHelper(y)}}-'></p>
 
 <script>
 	import counter from './counter.js';
 
 	export default {
 		helpers: {
-			getClass(value) {
-				counter.y += 1;
-				return value;
-			},
-
 			myHelper(value) {
-				counter.z += 1;
+				counter.count += 1;
 				return value;
 			}
 		}
diff --git a/test/runtime/samples/ignore-unchanged-attribute/_config.js b/test/runtime/samples/ignore-unchanged-attribute/_config.js
new file mode 100644
index 0000000000..eb62f5dd8e
--- /dev/null
+++ b/test/runtime/samples/ignore-unchanged-attribute/_config.js
@@ -0,0 +1,26 @@
+import counter from './counter.js';
+
+export default {
+	data: {
+		x: 1,
+		y: 2
+	},
+
+	html: `
+		<p>1</p>
+		<p class='2'></p>
+	`,
+
+	test(assert, component) {
+		counter.count = 0;
+
+		component.set({ x: 3 });
+		assert.equal(counter.count, 0);
+
+		component.set({ x: 4, y: 5 });
+		assert.equal(counter.count, 1);
+
+		component.set({ x: 5, y: 5 });
+		assert.equal(counter.count, 1);
+	}
+};
diff --git a/test/runtime/samples/ignore-unchanged-attribute/counter.js b/test/runtime/samples/ignore-unchanged-attribute/counter.js
new file mode 100644
index 0000000000..63872cd6a2
--- /dev/null
+++ b/test/runtime/samples/ignore-unchanged-attribute/counter.js
@@ -0,0 +1,3 @@
+export default {
+	count: 0
+};
\ No newline at end of file
diff --git a/test/runtime/samples/ignore-unchanged-attribute/main.html b/test/runtime/samples/ignore-unchanged-attribute/main.html
new file mode 100644
index 0000000000..afaa83c85f
--- /dev/null
+++ b/test/runtime/samples/ignore-unchanged-attribute/main.html
@@ -0,0 +1,15 @@
+<p>{{x}}</p>
+<p class='{{myHelper(y)}}'></p>
+
+<script>
+	import counter from './counter.js';
+
+	export default {
+		helpers: {
+			myHelper(value) {
+				counter.count += 1;
+				return value;
+			}
+		}
+	};
+</script>
diff --git a/test/runtime/samples/ignore-unchanged-raw/_config.js b/test/runtime/samples/ignore-unchanged-raw/_config.js
new file mode 100644
index 0000000000..b8c58367b3
--- /dev/null
+++ b/test/runtime/samples/ignore-unchanged-raw/_config.js
@@ -0,0 +1,26 @@
+import counter from './counter.js';
+
+export default {
+	data: {
+		x: 1,
+		y: 2
+	},
+
+	html: `
+		<p>1</p>
+		<p>2</p>
+	`,
+
+	test(assert, component) {
+		counter.count = 0;
+
+		component.set({ x: 3 });
+		assert.equal(counter.count, 0);
+
+		component.set({ x: 4, y: 5 });
+		assert.equal(counter.count, 1);
+
+		component.set({ x: 5, y: 5 });
+		assert.equal(counter.count, 1);
+	}
+};
diff --git a/test/runtime/samples/ignore-unchanged-raw/counter.js b/test/runtime/samples/ignore-unchanged-raw/counter.js
new file mode 100644
index 0000000000..63872cd6a2
--- /dev/null
+++ b/test/runtime/samples/ignore-unchanged-raw/counter.js
@@ -0,0 +1,3 @@
+export default {
+	count: 0
+};
\ No newline at end of file
diff --git a/test/runtime/samples/ignore-unchanged-raw/main.html b/test/runtime/samples/ignore-unchanged-raw/main.html
new file mode 100644
index 0000000000..375985c0d2
--- /dev/null
+++ b/test/runtime/samples/ignore-unchanged-raw/main.html
@@ -0,0 +1,15 @@
+<p>{{x}}</p>
+<p>{{{myHelper(y)}}}</p>
+
+<script>
+	import counter from './counter.js';
+
+	export default {
+		helpers: {
+			myHelper(value) {
+				counter.count += 1;
+				return value;
+			}
+		}
+	};
+</script>
diff --git a/test/runtime/samples/ignore-unchanged-tag/_config.js b/test/runtime/samples/ignore-unchanged-tag/_config.js
new file mode 100644
index 0000000000..b8c58367b3
--- /dev/null
+++ b/test/runtime/samples/ignore-unchanged-tag/_config.js
@@ -0,0 +1,26 @@
+import counter from './counter.js';
+
+export default {
+	data: {
+		x: 1,
+		y: 2
+	},
+
+	html: `
+		<p>1</p>
+		<p>2</p>
+	`,
+
+	test(assert, component) {
+		counter.count = 0;
+
+		component.set({ x: 3 });
+		assert.equal(counter.count, 0);
+
+		component.set({ x: 4, y: 5 });
+		assert.equal(counter.count, 1);
+
+		component.set({ x: 5, y: 5 });
+		assert.equal(counter.count, 1);
+	}
+};
diff --git a/test/runtime/samples/ignore-unchanged-tag/counter.js b/test/runtime/samples/ignore-unchanged-tag/counter.js
new file mode 100644
index 0000000000..63872cd6a2
--- /dev/null
+++ b/test/runtime/samples/ignore-unchanged-tag/counter.js
@@ -0,0 +1,3 @@
+export default {
+	count: 0
+};
\ No newline at end of file
diff --git a/test/runtime/samples/ignore-unchanged-tag/main.html b/test/runtime/samples/ignore-unchanged-tag/main.html
new file mode 100644
index 0000000000..f50864df94
--- /dev/null
+++ b/test/runtime/samples/ignore-unchanged-tag/main.html
@@ -0,0 +1,15 @@
+<p>{{x}}</p>
+<p>{{myHelper(y)}}</p>
+
+<script>
+	import counter from './counter.js';
+
+	export default {
+		helpers: {
+			myHelper(value) {
+				counter.count += 1;
+				return value;
+			}
+		}
+	};
+</script>