NYT Open

How we design and build digital products at The New York Times.

Follow publication

Enhancing The New York Times Web Performance with React 18

The NYT Open Team
NYT Open
Published in
11 min readJun 26, 2024
Illustration by Ben Hickey

Our Migration Process

Adapting Embedded Interactives to React 18

A custom embedded interactive built by our graphics developers: https://www.nytimes.com/article/hurricane-norma-baja-california.html
const embeddedInteractiveString = `
<div id="server-test">server</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
const serverTestElement = document.getElementById("server-test");
serverTestElement.textContent = "client";
});
</script>
`
;
return <div dangerouslySetInnerHTML={{ __html: embeddedInteractiveString }} />;

Extracting and Executing Embedded Interactive Scripts Manually

// This function replaces script tags in generic html with empty placeholders.
// This allows us to replace the script tag reference in-place later on client-mount with the actual script.
export const addsPlaceholderScript = (scriptText, id, scriptCounter) => {
let replacementToken = '';
let hoistedText = scriptText;

replacementToken = `<script id="${id}-script-${scriptCounter}"></script>`;
hoistedText = hoistedText.replace('<script', `<script id="${id}-script-${scriptCounter}"`);

return {
replacementToken,
hoistedText,
};
};

// This function extracts and removes `<script>` tags from an interactive HTML string
// and returns an object containing:
// - `scriptsToRunOnClient`: An array of script texts to be run on client-mount.
// - `scriptlessHtml`: The modified HTML string with scripts removed with empty script references.
export const extractAndReplace = (html, id) => {
const SCRIPT_REGEX = /<script[\s\S]*?>[\s\S]*?<\/script>/gi;
let lastMatchAdjustment = 0;
let scriptlessHtml = html;
let match;
const scriptsToRunOnClient = [];
let scriptCounter = 0;
while ((match = SCRIPT_REGEX.exec(html))) {
const [matchText] = match;
if (matchText) {
let hoistedText = matchText;
let replacementToken = '';
({ hoistedText, replacementToken } = addsPlaceholderScript(hoistedText, id, scriptCounter));
scriptCounter += 1;
const start = match.index - lastMatchAdjustment;
const end = match.index + matchText.length - lastMatchAdjustment;
scriptlessHtml = `${scriptlessHtml.substring(
0,
start
)}
${replacementToken}${scriptlessHtml.substring(end, scriptlessHtml.length)}`
;
scriptsToRunOnClient.push(hoistedText);
lastMatchAdjustment += matchText.length - replacementToken.length;
}
}

return {
scriptsToRunOnClient,
scriptlessHtml,
};
};

// Run script on client
const runScript = (clonedScript) => {
const script = document.getElementById(document.getElementById(`${clonedScript.id}`))
script.parentNode.replaceChild(clonedScript, script);
}
<script>
const
results = document.getElementById("RESULTS_MANIFEST").innerHTML.ELECTION_RESULTS;
// do additional logic with results
</script>
<div>
Interactive DOM Content Goes here
</>div>
<script id="RESULTS_MANIFEST>{"ELECTION_RESULTS": ['result1', 'result2', ....]}</script>
// Parses the provided script tag, returning a priority for sorting.
// Priority 1: for JSON or other metadata content.
// Priority 2: for other vanilla JS or src contents
export const getPriority = template => {
let priority;
try {
JSON.parse(template.innerHTML);
priority = 1;
} catch (err) {
priority = 2;
}
return priority;
};


scripts.sort((a, b) => getPriority(a) - getPriority(b));

Immediate Performance Benefits

Where We Go From Here

Published in NYT Open

How we design and build digital products at The New York Times.

Written by The NYT Open Team

We’re New York Times employees writing about workplace culture, and how we design and build digital products for journalism.

Responses (11)

Write a response