diff --git a/src/HelmetUtils.js b/src/HelmetUtils.js index 3abf32fe..fe854ba9 100644 --- a/src/HelmetUtils.js +++ b/src/HelmetUtils.js @@ -304,6 +304,27 @@ const handleClientStateChange = newState => { cancelIdleCallback(_helmetIdleCallback); } + const splitUpdates = { + baseTag: splitSyncAndDeferred(baseTag), + linkTags: splitSyncAndDeferred(linkTags), + metaTags: splitSyncAndDeferred(metaTags), + noscriptTags: splitSyncAndDeferred(noscriptTags), + scriptTags: splitSyncAndDeferred(scriptTags), + styleTags: splitSyncAndDeferred(styleTags) + }; + + const syncUpdates = { + baseTag: updateTags(TAG_NAMES.BASE, splitUpdates.baseTag.sync), + linkTags: updateTags(TAG_NAMES.LINK, splitUpdates.linkTags.sync), + metaTags: updateTags(TAG_NAMES.META, splitUpdates.metaTags.sync), + noscriptTags: updateTags( + TAG_NAMES.NOSCRIPT, + splitUpdates.noscriptTags.sync + ), + scriptTags: updateTags(TAG_NAMES.SCRIPT, splitUpdates.scriptTags.sync), + styleTags: updateTags(TAG_NAMES.STYLE, splitUpdates.styleTags.sync) + }; + _helmetIdleCallback = requestIdleCallback(() => { updateAttributes(TAG_NAMES.BODY, bodyAttributes); updateAttributes(TAG_NAMES.HTML, htmlAttributes); @@ -311,12 +332,27 @@ const handleClientStateChange = newState => { updateTitle(title, titleAttributes); const tagUpdates = { - baseTag: updateTags(TAG_NAMES.BASE, baseTag), - linkTags: updateTags(TAG_NAMES.LINK, linkTags), - metaTags: updateTags(TAG_NAMES.META, metaTags), - noscriptTags: updateTags(TAG_NAMES.NOSCRIPT, noscriptTags), - scriptTags: updateTags(TAG_NAMES.SCRIPT, scriptTags), - styleTags: updateTags(TAG_NAMES.STYLE, styleTags) + baseTag: syncUpdates.baseTag.concat( + updateTags(TAG_NAMES.BASE, splitUpdates.baseTag.deferred) + ), + linkTags: syncUpdates.linkTags.concat( + updateTags(TAG_NAMES.LINK, splitUpdates.linkTags.deferred) + ), + metaTags: syncUpdates.metaTags.concat( + updateTags(TAG_NAMES.META, splitUpdates.metaTags.deferred) + ), + noscriptTags: syncUpdates.noscriptTags.concat( + updateTags( + TAG_NAMES.NOSCRIPT, + splitUpdates.noscriptTags.deferred + ) + ), + scriptTags: syncUpdates.scriptTags.concat( + updateTags(TAG_NAMES.SCRIPT, splitUpdates.scriptTags.deferred) + ), + styleTags: syncUpdates.styleTags.concat( + updateTags(TAG_NAMES.STYLE, splitUpdates.styleTags.deferred) + ) }; const addedTags = {}; @@ -450,10 +486,68 @@ const updateTags = (type, tags) => { oldTags.forEach(tag => tag.parentNode.removeChild(tag)); newTags.forEach(tag => headElement.appendChild(tag)); - return { + return Update({ oldTags, newTags - }; + }); +}; + +// Helper object that implements concat method that takes care of handling +// intersection between old and new tags. (implements Semigroup) +const Update = ({oldTags, newTags}) => { + return Object.create(Object.prototype, { + oldTags: { + enumerable: true, + value: oldTags + }, + newTags: { + enumerable: true, + value: newTags + }, + concat: { + value: other => { + const mergedNewTags = newTags.concat(other.newTags); + const mergedOldTags = oldTags.concat(other.oldTags); + + // Remove intersection between old and new tags since they cancel + // each other out. + for (let i = 0; i < mergedNewTags.length; i++) { + for (let j = 0; j < mergedOldTags.length; j++) { + if (mergedNewTags[i].isEqualNode(mergedOldTags[j])) { + mergedNewTags.splice(i, 1); + mergedOldTags.splice(j, 1); + } + } + } + + return Update({ + newTags: mergedNewTags, + oldTags: mergedOldTags + }); + } + } + }); +}; + +const splitSyncAndDeferred = tags => { + return ( + tags && + tags.reduce( + (acc, tag) => { + // undefined counts as sync + if (tag.defer === false) { + acc.sync.push(tag); + } else { + acc.deferred.push(tag); + } + return acc; + }, + { + deferred: [], + sync: [] + } + ) + ); }; const generateElementAttributesAsString = attributes => diff --git a/test/HelmetDeclarativeTest.js b/test/HelmetDeclarativeTest.js index d7c3bd0b..ff06e2b6 100644 --- a/test/HelmetDeclarativeTest.js +++ b/test/HelmetDeclarativeTest.js @@ -2501,6 +2501,41 @@ describe("Helmet - Declarative API", () => { }); }); + describe("deferred tags", () => { + beforeEach(() => { + window.__spy__ = sinon.spy(); + }); + + afterEach(() => { + delete window.__spy__; + }); + + it("executes synchronously when defer={true} and async otherwise", done => { + ReactDOM.render( + + + + + , + container + ); + + expect(window.__spy__.callCount).to.equal(1); + + requestIdleCallback(() => { + expect(window.__spy__.callCount).to.equal(3); + expect(window.__spy__.args).to.deep.equal([[1], [2], [3]]); + done(); + }); + }); + }); + describe("server", () => { const stringifiedHtmlAttributes = `lang="ga" class="myClassName"`; const stringifiedBodyAttributes = `lang="ga" class="myClassName"`; diff --git a/test/HelmetTest.js b/test/HelmetTest.js index 43d02ff2..594be402 100644 --- a/test/HelmetTest.js +++ b/test/HelmetTest.js @@ -2271,6 +2271,45 @@ describe("Helmet", () => { }); }); + describe("deferred tags", () => { + beforeEach(() => { + window.__spy__ = sinon.spy(); + }); + + afterEach(() => { + delete window.__spy__; + }); + + it("executes synchronously when defer={true} and async otherwise", done => { + ReactDOM.render( + , + container + ); + + expect(window.__spy__.callCount).to.equal(1); + + requestIdleCallback(() => { + expect(window.__spy__.callCount).to.equal(3); + expect(window.__spy__.args).to.deep.equal([[1], [2], [3]]); + done(); + }); + }); + }); + describe("server", () => { const stringifiedHtmlAttributes = `lang="ga" class="myClassName"`; const stringifiedTitle = `Dangerous <script> include`;