From 19798b5cce0c525ec52b108725e30f13beb62c17 Mon Sep 17 00:00:00 2001 From: Jude Agboola Date: Fri, 22 Jul 2022 09:51:57 +0100 Subject: [PATCH 01/12] use innerHTML since since text is unesacaped --- .../head-function-export/__tests__/ssr-html-output.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-tests/head-function-export/__tests__/ssr-html-output.js b/integration-tests/head-function-export/__tests__/ssr-html-output.js index 74ab79ac30afb..dfe81e001462f 100644 --- a/integration-tests/head-function-export/__tests__/ssr-html-output.js +++ b/integration-tests/head-function-export/__tests__/ssr-html-output.js @@ -34,7 +34,7 @@ describe(`Head function export SSR'ed HTML output`, () => { expect(noscript.text).toEqual(data.static.noscript) expect(style.text).toContain(data.static.style) expect(link.attributes.href).toEqual(data.static.link) - expect(jsonLD.text).toEqual(data.static.jsonLD) + expect(jsonLD.innerHTML).toEqual(data.static.jsonLD) }) it(`should work with data from a page query`, () => { From 71929893617d048415bceb3e0ab7dc1d35b6fa75 Mon Sep 17 00:00:00 2001 From: Jude Agboola Date: Fri, 22 Jul 2022 11:36:09 +0100 Subject: [PATCH 02/12] make scripts run --- .../head/head-export-handler-for-browser.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js b/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js index 94d42f4ee7b02..47b542d425e36 100644 --- a/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js +++ b/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js @@ -31,8 +31,19 @@ const onHeadRendered = () => { if (!VALID_NODE_NAMES.includes(nodeName)) { warnForInvalidTags(nodeName) } else { - const clonedNode = node.cloneNode(true) + let clonedNode = node.cloneNode(true) clonedNode.setAttribute(`data-gatsby-head`, true) + + // Create an element for scripts to make script work + if (clonedNode.nodeName.toLowerCase() === `script`) { + const script = document.createElement(`script`) + for (const attr of clonedNode.attributes) { + script.setAttribute(attr.name, attr.value) + } + script.innerHTML = clonedNode.innerHTML + clonedNode = script + } + if (id) { if (!seenIds.has(id)) { validHeadNodes.push(clonedNode) From 0a8987d74aca4868d5db5eee7ce4ea06346b7e65 Mon Sep 17 00:00:00 2001 From: Jude Agboola Date: Fri, 22 Jul 2022 16:11:22 +0100 Subject: [PATCH 03/12] eliminate multiple append calls --- .../cache-dir/head/head-export-handler-for-browser.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js b/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js index 47b542d425e36..dabd29872b754 100644 --- a/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js +++ b/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js @@ -12,6 +12,7 @@ import { } from "./utils" const hiddenRoot = document.createElement(`div`) +let prevInnerHTML = `` const removePrevHeadElements = () => { const prevHeadNodes = [...document.querySelectorAll(`[data-gatsby-head]`)] @@ -21,6 +22,14 @@ const removePrevHeadElements = () => { const onHeadRendered = () => { const validHeadNodes = [] + if (!prevInnerHTML) { + prevInnerHTML = hiddenRoot.innerHTML + } else { + if (prevInnerHTML === hiddenRoot.innerHTML) { + return + } + } + removePrevHeadElements() const seenIds = new Map() From 9ffe392d5cc37f4d0504803ca7329fc3445d8fd5 Mon Sep 17 00:00:00 2001 From: Jude Agboola Date: Fri, 22 Jul 2022 16:13:02 +0100 Subject: [PATCH 04/12] dev inline script test --- .../head-function-export/script-tags.js | 18 ++++++++++++++++++ .../src/pages/head-function-export/basic.js | 5 +++++ 2 files changed, 23 insertions(+) create mode 100644 e2e-tests/development-runtime/cypress/integration/head-function-export/script-tags.js diff --git a/e2e-tests/development-runtime/cypress/integration/head-function-export/script-tags.js b/e2e-tests/development-runtime/cypress/integration/head-function-export/script-tags.js new file mode 100644 index 0000000000000..2581510071866 --- /dev/null +++ b/e2e-tests/development-runtime/cypress/integration/head-function-export/script-tags.js @@ -0,0 +1,18 @@ +import { page } from "../../../shared-data/head-function-export.js" + +describe("scripts tags", () => { + beforeEach(() => { + cy.visit(page.basic).waitForRouteChange() + }) + + it(`Inline scripts in Head export works`, () => { + cy.get(`@hmrConsoleLog`).should(`be.have.callCount`, 2) + + + cy.get(`@hmrConsoleLog`).should( + `be.calledWithExactly`, + `[HMR] connected`, + `I am an inline script` + ) + }) +}) diff --git a/e2e-tests/development-runtime/src/pages/head-function-export/basic.js b/e2e-tests/development-runtime/src/pages/head-function-export/basic.js index af5204c87420a..c40b9e023da7c 100644 --- a/e2e-tests/development-runtime/src/pages/head-function-export/basic.js +++ b/e2e-tests/development-runtime/src/pages/head-function-export/basic.js @@ -54,6 +54,11 @@ export function Head() { + ) } From dd61e7a6bf5a2e6bd4fb186030faa2b72da9ae7e Mon Sep 17 00:00:00 2001 From: Jude Agboola Date: Tue, 26 Jul 2022 10:54:22 +0100 Subject: [PATCH 05/12] fix problematic multiple appending --- .../head/head-export-handler-for-browser.js | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js b/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js index dabd29872b754..a6f428905160a 100644 --- a/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js +++ b/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js @@ -13,6 +13,7 @@ import { const hiddenRoot = document.createElement(`div`) let prevInnerHTML = `` +let prevPagePath = `` const removePrevHeadElements = () => { const prevHeadNodes = [...document.querySelectorAll(`[data-gatsby-head]`)] @@ -20,16 +21,24 @@ const removePrevHeadElements = () => { } const onHeadRendered = () => { - const validHeadNodes = [] - - if (!prevInnerHTML) { - prevInnerHTML = hiddenRoot.innerHTML - } else { - if (prevInnerHTML === hiddenRoot.innerHTML) { - return - } + /** + * Bailout if page path and innerHTML did not change + * + * This function gets called by mutation observer and useEffect. + * This early bailout makes sure we don't run this callback twice for the same page. + */ + if ( + prevInnerHTML === hiddenRoot.innerHTML && + prevPagePath === window.location.pathname + ) { + return } + prevInnerHTML = hiddenRoot.innerHTML + prevPagePath = window.location.pathname + + const validHeadNodes = [] + removePrevHeadElements() const seenIds = new Map() From 9a87b233c1be8e3f3efd32fd836480fc02bec7fa Mon Sep 17 00:00:00 2001 From: Jude Agboola Date: Tue, 26 Jul 2022 10:55:21 +0100 Subject: [PATCH 06/12] test that script run and get called once --- .../head-function-export/script-tags.js | 18 ----------------- .../head-function-export/scripts.js | 20 +++++++++++++++++++ .../src/pages/head-function-export/basic.js | 15 +++++++------- .../head-function-export/scripts.js | 20 +++++++++++++++++++ .../src/pages/head-function-export/basic.js | 3 +++ 5 files changed, 51 insertions(+), 25 deletions(-) delete mode 100644 e2e-tests/development-runtime/cypress/integration/head-function-export/script-tags.js create mode 100644 e2e-tests/development-runtime/cypress/integration/head-function-export/scripts.js create mode 100644 e2e-tests/production-runtime/cypress/integration/head-function-export/scripts.js diff --git a/e2e-tests/development-runtime/cypress/integration/head-function-export/script-tags.js b/e2e-tests/development-runtime/cypress/integration/head-function-export/script-tags.js deleted file mode 100644 index 2581510071866..0000000000000 --- a/e2e-tests/development-runtime/cypress/integration/head-function-export/script-tags.js +++ /dev/null @@ -1,18 +0,0 @@ -import { page } from "../../../shared-data/head-function-export.js" - -describe("scripts tags", () => { - beforeEach(() => { - cy.visit(page.basic).waitForRouteChange() - }) - - it(`Inline scripts in Head export works`, () => { - cy.get(`@hmrConsoleLog`).should(`be.have.callCount`, 2) - - - cy.get(`@hmrConsoleLog`).should( - `be.calledWithExactly`, - `[HMR] connected`, - `I am an inline script` - ) - }) -}) diff --git a/e2e-tests/development-runtime/cypress/integration/head-function-export/scripts.js b/e2e-tests/development-runtime/cypress/integration/head-function-export/scripts.js new file mode 100644 index 0000000000000..e078130b28e1b --- /dev/null +++ b/e2e-tests/development-runtime/cypress/integration/head-function-export/scripts.js @@ -0,0 +1,20 @@ +import { page } from "../../../shared-data/head-function-export.js" + +describe("Scripts", () => { + beforeEach(() => { + cy.visit(page.basic).waitForRouteChange() + }) + + // This tests that we don't append elements to the document head more than once + // A script will get called more than once it that happens + it(`Inline script work and get called only once`, () => { + + // Head export seem to be appending the tags after waitForRouteChange() + // We need to find a way to make waitForRouteChange() catch Head export too + cy.wait(3000) + + cy.window().then(win => { + expect(win.__SOME_GLOBAL_TO_CHECK_CALL_COUNT__).to.equal(1) + }) + }) +}) diff --git a/e2e-tests/development-runtime/src/pages/head-function-export/basic.js b/e2e-tests/development-runtime/src/pages/head-function-export/basic.js index c40b9e023da7c..a79609b1f240d 100644 --- a/e2e-tests/development-runtime/src/pages/head-function-export/basic.js +++ b/e2e-tests/development-runtime/src/pages/head-function-export/basic.js @@ -12,7 +12,10 @@ export default function HeadFunctionExportBasic() { Navigate to page-query via Gatsby Link - + Navigate to without head export @@ -28,7 +31,7 @@ export function Head() { style, link, extraMeta, - jsonLD + jsonLD, } = data.static return ( @@ -54,11 +57,9 @@ export function Head() { - + ) } diff --git a/e2e-tests/production-runtime/cypress/integration/head-function-export/scripts.js b/e2e-tests/production-runtime/cypress/integration/head-function-export/scripts.js new file mode 100644 index 0000000000000..e078130b28e1b --- /dev/null +++ b/e2e-tests/production-runtime/cypress/integration/head-function-export/scripts.js @@ -0,0 +1,20 @@ +import { page } from "../../../shared-data/head-function-export.js" + +describe("Scripts", () => { + beforeEach(() => { + cy.visit(page.basic).waitForRouteChange() + }) + + // This tests that we don't append elements to the document head more than once + // A script will get called more than once it that happens + it(`Inline script work and get called only once`, () => { + + // Head export seem to be appending the tags after waitForRouteChange() + // We need to find a way to make waitForRouteChange() catch Head export too + cy.wait(3000) + + cy.window().then(win => { + expect(win.__SOME_GLOBAL_TO_CHECK_CALL_COUNT__).to.equal(1) + }) + }) +}) diff --git a/e2e-tests/production-runtime/src/pages/head-function-export/basic.js b/e2e-tests/production-runtime/src/pages/head-function-export/basic.js index 1132b579f8cda..e2bad20c6174f 100644 --- a/e2e-tests/production-runtime/src/pages/head-function-export/basic.js +++ b/e2e-tests/production-runtime/src/pages/head-function-export/basic.js @@ -38,6 +38,9 @@ export function Head() { + ) } From 3e660c1ba3243f0c5e148ee102070bacc42d90e5 Mon Sep 17 00:00:00 2001 From: Jude Agboola Date: Wed, 27 Jul 2022 13:50:19 +0100 Subject: [PATCH 07/12] diff nodes before reappending --- .../head/head-export-handler-for-browser.js | 39 +++++++++---------- packages/gatsby/cache-dir/head/utils.js | 30 ++++++++++++++ 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js b/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js index a6f428905160a..1665d09662f9e 100644 --- a/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js +++ b/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js @@ -9,11 +9,10 @@ import { headExportValidator, filterHeadProps, warnForInvalidTags, + isEqualNode, } from "./utils" const hiddenRoot = document.createElement(`div`) -let prevInnerHTML = `` -let prevPagePath = `` const removePrevHeadElements = () => { const prevHeadNodes = [...document.querySelectorAll(`[data-gatsby-head]`)] @@ -21,26 +20,8 @@ const removePrevHeadElements = () => { } const onHeadRendered = () => { - /** - * Bailout if page path and innerHTML did not change - * - * This function gets called by mutation observer and useEffect. - * This early bailout makes sure we don't run this callback twice for the same page. - */ - if ( - prevInnerHTML === hiddenRoot.innerHTML && - prevPagePath === window.location.pathname - ) { - return - } - - prevInnerHTML = hiddenRoot.innerHTML - prevPagePath = window.location.pathname - const validHeadNodes = [] - removePrevHeadElements() - const seenIds = new Map() for (const node of hiddenRoot.childNodes) { const nodeName = node.nodeName.toLowerCase() @@ -77,7 +58,23 @@ const onHeadRendered = () => { } } - document.head.append(...validHeadNodes) + const diffedHeadNodes = [] + + const existingHeadElements = [ + ...document.querySelectorAll(`[data-gatsby-head]`), + ] + + for (const validHeadNode of validHeadNodes) { + const isInExistingHead = existingHeadElements.some(e => + isEqualNode(e, validHeadNode) + ) + + if (!isInExistingHead) { + diffedHeadNodes.push(validHeadNode) + } + } + + document.head.append(...diffedHeadNodes) } if (process.env.BUILD_STAGE === `develop`) { diff --git a/packages/gatsby/cache-dir/head/utils.js b/packages/gatsby/cache-dir/head/utils.js index 833226a37f90b..aaf0d27a2b9ec 100644 --- a/packages/gatsby/cache-dir/head/utils.js +++ b/packages/gatsby/cache-dir/head/utils.js @@ -52,3 +52,33 @@ export function warnForInvalidTags(tagName) { warnOnce(warning) } } + +/** + * When a `nonce` is present on an element, browsers such as Chrome and Firefox strip it out of the + * actual HTML attributes for security reasons *when the element is added to the document*. Thus, + * given two equivalent elements that have nonces, `Element,isEqualNode()` will return false if one + * of those elements gets added to the document. Although the `element.nonce` property will be the + * same for both elements, the one that was added to the document will return an empty string for + * its nonce HTML attribute value. + * + * This custom `isEqualNode()` function therefore removes the nonce value from the `newTag` before + * comparing it to `oldTag`, restoring it afterwards. + * + * For more information, see: + * https://bugs.chromium.org/p/chromium/issues/detail?id=1211471#c12 + */ +export function isEqualNode(oldTag, newTag) { + if (oldTag instanceof HTMLElement && newTag instanceof HTMLElement) { + const nonce = newTag.getAttribute(`nonce`) + // Only strip the nonce if `oldTag` has had it stripped. An element's nonce attribute will not + // be stripped if there is no content security policy response header that includes a nonce. + if (nonce && !oldTag.getAttribute(`nonce`)) { + const cloneTag = newTag.cloneNode(true) + cloneTag.setAttribute(`nonce`, ``) + cloneTag.nonce = nonce + return nonce === oldTag.nonce && oldTag.isEqualNode(cloneTag) + } + } + + return oldTag.isEqualNode(newTag) +} From ae3d0d6a622e5807a9e1b0ed2b2c4bd45a0217c1 Mon Sep 17 00:00:00 2001 From: Jude Agboola Date: Wed, 27 Jul 2022 17:38:10 +0100 Subject: [PATCH 08/12] add new elements and remove stale ones --- .../head/head-export-handler-for-browser.js | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js b/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js index 1665d09662f9e..b865392270549 100644 --- a/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js +++ b/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js @@ -64,12 +64,31 @@ const onHeadRendered = () => { ...document.querySelectorAll(`[data-gatsby-head]`), ] + if (existingHeadElements.length === 0) { + document.head.append(...validHeadNodes) + return + } + + const elementsToRemove = [] + + for (const existingHeadElement of existingHeadElements) { + const isInValidNodes = validHeadNodes.some(e => + isEqualNode(e, existingHeadElement) + ) + + if (!isInValidNodes) { + elementsToRemove.push(existingHeadElement) + } + } + + elementsToRemove.forEach(e => e.remove()) + for (const validHeadNode of validHeadNodes) { - const isInExistingHead = existingHeadElements.some(e => + const isInExistingHeadElementsList = existingHeadElements.some(e => isEqualNode(e, validHeadNode) ) - if (!isInExistingHead) { + if (!isInExistingHeadElementsList) { diffedHeadNodes.push(validHeadNode) } } From 408eaadb6804dc25931092676d6734f4d202c456 Mon Sep 17 00:00:00 2001 From: Jude Agboola Date: Mon, 1 Aug 2022 13:07:13 +0100 Subject: [PATCH 09/12] remove element in prev loop --- .../head/head-export-handler-for-browser.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js b/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js index b865392270549..94686aa97cb80 100644 --- a/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js +++ b/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js @@ -58,7 +58,7 @@ const onHeadRendered = () => { } } - const diffedHeadNodes = [] + const newHeadNodes = [] const existingHeadElements = [ ...document.querySelectorAll(`[data-gatsby-head]`), @@ -69,31 +69,27 @@ const onHeadRendered = () => { return } - const elementsToRemove = [] - for (const existingHeadElement of existingHeadElements) { const isInValidNodes = validHeadNodes.some(e => isEqualNode(e, existingHeadElement) ) if (!isInValidNodes) { - elementsToRemove.push(existingHeadElement) + existingHeadElement.remove() } } - elementsToRemove.forEach(e => e.remove()) - for (const validHeadNode of validHeadNodes) { const isInExistingHeadElementsList = existingHeadElements.some(e => isEqualNode(e, validHeadNode) ) if (!isInExistingHeadElementsList) { - diffedHeadNodes.push(validHeadNode) + newHeadNodes.push(validHeadNode) } } - document.head.append(...diffedHeadNodes) + document.head.append(...newHeadNodes) } if (process.env.BUILD_STAGE === `develop`) { From fb1a6c91e3570a778abaa0189eb4d7a9bc71b5cc Mon Sep 17 00:00:00 2001 From: pieh Date: Mon, 1 Aug 2022 16:21:39 +0200 Subject: [PATCH 10/12] move node diffind to separate function, add unit test --- .../gatsby/cache-dir/head/__tests__/utils.ts | 74 +++++++++++++++++++ .../head/head-export-handler-for-browser.js | 27 ++----- packages/gatsby/cache-dir/head/utils.js | 22 ++++++ 3 files changed, 103 insertions(+), 20 deletions(-) create mode 100644 packages/gatsby/cache-dir/head/__tests__/utils.ts diff --git a/packages/gatsby/cache-dir/head/__tests__/utils.ts b/packages/gatsby/cache-dir/head/__tests__/utils.ts new file mode 100644 index 0000000000000..c4557d2898d86 --- /dev/null +++ b/packages/gatsby/cache-dir/head/__tests__/utils.ts @@ -0,0 +1,74 @@ +/** + * @jest-environment jsdom + */ + +import { diffNodes } from "../utils" + +function createElement( + type: string, + attributes: Record | undefined = undefined, + innerHTML: string | undefined = undefined +): Element { + const element: Element = document.createElement(type) + if (attributes) { + for (const [key, value] of Object.entries(attributes)) { + if (value === `string`) { + element.setAttribute(key, value) + } + } + } + if (innerHTML) { + element.innerHTML = innerHTML + } + return element +} + +describe(`diffNodes`, () => { + it(`should keep same nodes, remove nodes that were not re-created, and add new nodes`, () => { + const oldNodes = [ + createElement(`title`, {}, `to remove`), + createElement(`script`, {}, `stable`), + createElement(`script`, {}, `to remove`), + ] + + const newNodes = [ + createElement(`title`, {}, `to add`), + createElement(`script`, {}, `stable`), + createElement(`script`, {}, `to add`), + ] + + const onStale = jest.fn() + const onNew = jest.fn() + + diffNodes({ oldNodes, newNodes, onStale, onNew }) + + expect(onStale.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + + to remove + , + ], + Array [ + , + ], + ] + `) + expect(onNew.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + + to add + , + ], + Array [ + , + ], + ] + `) + }) +}) diff --git a/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js b/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js index 94686aa97cb80..25d78bab9c6de 100644 --- a/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js +++ b/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js @@ -9,7 +9,7 @@ import { headExportValidator, filterHeadProps, warnForInvalidTags, - isEqualNode, + diffNodes, } from "./utils" const hiddenRoot = document.createElement(`div`) @@ -69,25 +69,12 @@ const onHeadRendered = () => { return } - for (const existingHeadElement of existingHeadElements) { - const isInValidNodes = validHeadNodes.some(e => - isEqualNode(e, existingHeadElement) - ) - - if (!isInValidNodes) { - existingHeadElement.remove() - } - } - - for (const validHeadNode of validHeadNodes) { - const isInExistingHeadElementsList = existingHeadElements.some(e => - isEqualNode(e, validHeadNode) - ) - - if (!isInExistingHeadElementsList) { - newHeadNodes.push(validHeadNode) - } - } + diffNodes({ + oldNodes: existingHeadElements, + newNodes: validHeadNodes, + onStale: node => node.remove(), + onNew: node => newHeadNodes.push(node), + }) document.head.append(...newHeadNodes) } diff --git a/packages/gatsby/cache-dir/head/utils.js b/packages/gatsby/cache-dir/head/utils.js index aaf0d27a2b9ec..e967dee140563 100644 --- a/packages/gatsby/cache-dir/head/utils.js +++ b/packages/gatsby/cache-dir/head/utils.js @@ -82,3 +82,25 @@ export function isEqualNode(oldTag, newTag) { return oldTag.isEqualNode(newTag) } + +export function diffNodes({ oldNodes, newNodes, onStale, onNew }) { + for (const existingHeadElement of oldNodes) { + const isInValidNodes = newNodes.some(e => + isEqualNode(e, existingHeadElement) + ) + + if (!isInValidNodes) { + onStale(existingHeadElement) + } + } + + for (const validHeadNode of newNodes) { + const isInExistingHeadElementsList = oldNodes.some(e => + isEqualNode(e, validHeadNode) + ) + + if (!isInExistingHeadElementsList) { + onNew(validHeadNode) + } + } +} From add07ef5652f07e15fad6983eebc497178781157 Mon Sep 17 00:00:00 2001 From: pieh Date: Mon, 1 Aug 2022 16:25:49 +0200 Subject: [PATCH 11/12] slightly more performant diffing --- packages/gatsby/cache-dir/head/utils.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/gatsby/cache-dir/head/utils.js b/packages/gatsby/cache-dir/head/utils.js index e967dee140563..1897c81595551 100644 --- a/packages/gatsby/cache-dir/head/utils.js +++ b/packages/gatsby/cache-dir/head/utils.js @@ -85,22 +85,20 @@ export function isEqualNode(oldTag, newTag) { export function diffNodes({ oldNodes, newNodes, onStale, onNew }) { for (const existingHeadElement of oldNodes) { - const isInValidNodes = newNodes.some(e => + const indexInNewNodes = newNodes.findIndex(e => isEqualNode(e, existingHeadElement) ) - if (!isInValidNodes) { + if (indexInNewNodes === -1) { onStale(existingHeadElement) + } else { + // this node is re-created as-is, so we keep old node, and remove it from list of new nodes (as we handled it already here) + newNodes.splice(indexInNewNodes, 1) } } - for (const validHeadNode of newNodes) { - const isInExistingHeadElementsList = oldNodes.some(e => - isEqualNode(e, validHeadNode) - ) - - if (!isInExistingHeadElementsList) { - onNew(validHeadNode) - } + // remaing new nodes didn't have matching old node, so need to be added + for (const newNode of newNodes) { + onNew(newNode) } } From 364e11bc08c3be477d3a28b27e2dd5c74d8cb5e1 Mon Sep 17 00:00:00 2001 From: pieh Date: Mon, 1 Aug 2022 16:26:48 +0200 Subject: [PATCH 12/12] don't init array if we will bail early anyway --- .../gatsby/cache-dir/head/head-export-handler-for-browser.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js b/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js index 25d78bab9c6de..7f1866fbd5c40 100644 --- a/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js +++ b/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js @@ -58,8 +58,6 @@ const onHeadRendered = () => { } } - const newHeadNodes = [] - const existingHeadElements = [ ...document.querySelectorAll(`[data-gatsby-head]`), ] @@ -69,6 +67,7 @@ const onHeadRendered = () => { return } + const newHeadNodes = [] diffNodes({ oldNodes: existingHeadElements, newNodes: validHeadNodes,