Skip to content

Commit

Permalink
Parallelize Placeholder loading for an LCP improvement (#2752)
Browse files Browse the repository at this point in the history
mwpw-156840: parallelize loading placeholders
  • Loading branch information
mokimo authored Sep 2, 2024
1 parent 3331e11 commit 3ad052c
Show file tree
Hide file tree
Showing 13 changed files with 100 additions and 74 deletions.
3 changes: 1 addition & 2 deletions libs/blocks/chart/chart.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { loadScript, getConfig, createTag } from '../../utils/utils.js';
import { loadScript, getConfig, createTag, customFetch } from '../../utils/utils.js';
import {
throttle,
parseValue,
Expand Down Expand Up @@ -122,7 +122,6 @@ export function processMarkData(series, xUnit) {
}

export async function fetchData(link) {
const { customFetch } = await import('../../utils/helpers.js');
const resp = await customFetch({ resource: link.href.toLowerCase(), withCacheRules: true })
.catch(() => ({}));

Expand Down
3 changes: 1 addition & 2 deletions libs/blocks/fragment/fragment.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable max-classes-per-file */
import { createTag, getConfig, loadArea, localizeLink } from '../../utils/utils.js';
import { createTag, getConfig, loadArea, localizeLink, customFetch } from '../../utils/utils.js';

const fragMap = {};

Expand Down Expand Up @@ -83,7 +83,6 @@ export default async function init(a) {
return;
}

const { customFetch } = await import('../../utils/helpers.js');
let resourcePath = a.href;
if (a.href.includes('/federal/')) {
const { getFederatedUrl } = await import('../../utils/federated.js');
Expand Down
65 changes: 42 additions & 23 deletions libs/features/placeholders.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { customFetch, getConfig } from '../utils/utils.js';

const fetchedPlaceholders = {};
window.mph = {};

Expand All @@ -7,26 +9,23 @@ const getPlaceholdersPath = (config, sheet) => {
return `${path}${query}`;
};

const fetchPlaceholders = async (config, sheet) => {
const placeholdersPath = getPlaceholdersPath(config, sheet);
const { customFetch } = await import('../utils/helpers.js');

fetchedPlaceholders[placeholdersPath] = fetchedPlaceholders[placeholdersPath]
// eslint-disable-next-line no-async-promise-executor
|| new Promise(async (resolve) => {
const resp = await customFetch({ resource: placeholdersPath, withCacheRules: true })
.catch(() => ({}));
const json = resp.ok ? await resp.json() : { data: [] };
if (json.data.length === 0) { resolve({}); return; }
const placeholders = {};
json.data.forEach((item) => {
placeholders[item.key] = item.value;
window.mph[item.key] = item.value;
});
resolve(placeholders);
const fetchPlaceholders = async ({ config, sheet, placeholderRequest, placeholderPath }) => {
const path = placeholderPath || getPlaceholdersPath(config, sheet);
// eslint-disable-next-line no-async-promise-executor
fetchedPlaceholders[path] = fetchedPlaceholders[path] || new Promise(async (resolve) => {
const resp = await placeholderRequest || await customFetch(
{ resource: path, withCacheRules: true },
).catch(() => ({}));
const json = resp.ok ? await resp.json() : { data: [] };
if (json.data.length === 0) { resolve({}); return; }
const placeholders = {};
json.data.forEach((item) => {
window.mph[item.key] = item.value;
placeholders[item.key] = item.value;
});

return fetchedPlaceholders[placeholdersPath];
resolve(placeholders);
});
return fetchedPlaceholders[path];
};

function keyToStr(key) {
Expand Down Expand Up @@ -60,15 +59,15 @@ async function getPlaceholder(key, config, sheet) {
},
};

const defaultPlaceholders = await fetchPlaceholders(defaultConfig, sheet)
const defaultPlaceholders = await fetchPlaceholders({ config: defaultConfig, sheet })
.catch(() => ({}));
defaultFetched = true;
return defaultPlaceholders;
};

if (config.placeholders?.[key]) return config.placeholders[key];

const placeholders = await fetchPlaceholders(config, sheet).catch(async () => {
const placeholders = await fetchPlaceholders({ config, sheet }).catch(async () => {
const defaultPlaceholders = await getDefaultPlaceholders();
return defaultPlaceholders;
});
Expand Down Expand Up @@ -102,18 +101,38 @@ export async function replaceKeyArray(keys, config, sheet = 'default') {
return placeholders;
}

export async function replaceText(text, config, regex = /{{(.*?)}}|%7B%7B(.*?)%7D%7D/g, sheet = 'default') {
export async function replaceText(
text,
config,
regex = /{{(.*?)}}|%7B%7B(.*?)%7D%7D/g,
sheet = 'default',
) {
if (typeof text !== 'string' || !text.length) return '';

const matches = [...text.matchAll(new RegExp(regex))];
if (!matches.length) {
return text;
}
const keys = Array.from(matches, (match) => (match[1] || match[2]));
const keys = Array.from(matches, (match) => match[1] || match[2]);
const placeholders = await replaceKeyArray(keys, config, sheet);
// The .shift method is very slow, thus using normal iterator
let i = 0;
// eslint-disable-next-line no-plusplus
const finalText = text.replaceAll(regex, () => placeholders[i++]);
return finalText;
}

export async function decoratePlaceholderArea({
placeholderPath,
placeholderRequest,
nodes,
}) {
if (!nodes.length) return;
const config = getConfig();
await fetchPlaceholders({ placeholderPath, config, placeholderRequest });
const replaceNodes = nodes.map(async (textNode) => {
textNode.nodeValue = await replaceText(textNode.nodeValue, config);
textNode.nodeValue = textNode.nodeValue.replace(/ /g, '\u00A0');
});
await Promise.all(replaceNodes);
}
9 changes: 0 additions & 9 deletions libs/utils/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,3 @@ export function updateLinkWithLangRoot(link) {
return link;
}
}

export async function customFetch({ resource, withCacheRules }) {
const options = {};
if (withCacheRules) {
const params = new URLSearchParams(window.location.search);
options.cache = params.get('cache') === 'off' ? 'reload' : 'default';
}
return fetch(resource, options);
}
42 changes: 29 additions & 13 deletions libs/utils/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -727,15 +727,25 @@ async function decorateIcons(area, config) {
await loadIcons(icons, config);
}

async function decoratePlaceholders(area, config) {
const el = area.querySelector('main') || area;
export async function customFetch({ resource, withCacheRules }) {
const options = {};
if (withCacheRules) {
const params = new URLSearchParams(window.location.search);
options.cache = params.get('cache') === 'off' ? 'reload' : 'default';
}
return fetch(resource, options);
}

const findReplaceableNodes = (area) => {
const regex = /{{(.*?)}}|%7B%7B(.*?)%7D%7D/g;
const walker = document.createTreeWalker(
el,
area,
NodeFilter.SHOW_TEXT,
{
acceptNode(node) {
const a = regex.test(node.nodeValue) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
const a = regex.test(node.nodeValue)
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_REJECT;
regex.lastIndex = 0;
return a;
},
Expand All @@ -747,13 +757,20 @@ async function decoratePlaceholders(area, config) {
nodes.push(node);
node = walker.nextNode();
}
return nodes;
};

let placeholderRequest;
async function decoratePlaceholders(area, config) {
if (!area) return;
const nodes = findReplaceableNodes(area);
if (!nodes.length) return;
const { replaceText } = await import('../features/placeholders.js');
const replaceNodes = nodes.map(async (textNode) => {
textNode.nodeValue = await replaceText(textNode.nodeValue, config, regex);
textNode.nodeValue = textNode.nodeValue.replace(/ /g, '\u00A0');
});
await Promise.all(replaceNodes);
const placeholderPath = `${config.locale?.contentRoot}/placeholders.json`;
placeholderRequest = placeholderRequest
|| customFetch({ resource: placeholderPath, withCacheRules: true })
.catch(() => ({}));
const { decoratePlaceholderArea } = await import('../features/placeholders.js');
await decoratePlaceholderArea({ placeholderPath, placeholderRequest, nodes });
}

async function loadFooter() {
Expand Down Expand Up @@ -987,6 +1004,7 @@ async function checkForPageMods() {
}

async function loadPostLCP(config) {
await decoratePlaceholders(document.body.querySelector('header'), config);
if (config.mep?.targetEnabled === 'gnav') {
/* c8 ignore next 2 */
const { init } = await import('../features/personalization/personalization.js');
Expand Down Expand Up @@ -1174,11 +1192,11 @@ async function processSection(section, config, isDoc) {
const { default: loadInlineFrags } = await import('../blocks/fragment/fragment.js');
const fragPromises = inlineFrags.map((link) => loadInlineFrags(link));
await Promise.all(fragPromises);
await decoratePlaceholders(section.el, config);
const newlyDecoratedSection = decorateSection(section.el, section.idx);
section.blocks = newlyDecoratedSection.blocks;
section.preloadLinks = newlyDecoratedSection.preloadLinks;
}
await decoratePlaceholders(section.el, config);

if (section.preloadLinks.length) {
const [modals, nonModals] = partition(section.preloadLinks, (block) => block.classList.contains('modal'));
Expand Down Expand Up @@ -1215,8 +1233,6 @@ export async function loadArea(area = document) {
}
const config = getConfig();

await decoratePlaceholders(area, config);

if (isDoc) {
decorateDocumentExtras();
}
Expand Down
2 changes: 2 additions & 0 deletions test/blocks/caas/mocks/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export const utf8ToB64 = (str) => window.btoa(unescape(encodeURIComponent(str)))

export const b64ToUtf8 = (str) => decodeURIComponent(escape(window.atob(str)));

export const customFetch = stub();

export function getMetadata(name, doc = document) {
const attr = name && name.includes(':') ? 'property' : 'name';
const meta = doc.head.querySelector(`meta[${attr}="${name}"]`);
Expand Down
2 changes: 2 additions & 0 deletions test/blocks/instagram/mocks/embed-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export function createTag(tag, attributes, html) {

export const getConfig = () => ({});

export const customFetch = stub();

export const loadStyle = stub();

export const loadScript = stub();
Expand Down
2 changes: 2 additions & 0 deletions test/blocks/marketo/mocks/marketo-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,5 @@ export const localizeLink = (href) => href;
export const loadLink = stub().returns(new Promise((resolve) => {
resolve();
}));

export const customFetch = stub();
2 changes: 2 additions & 0 deletions test/blocks/merch/mocks/embed-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export const getConfig = () => config;

export const setConfig = (c) => { config = c; };

export const customFetch = stub();

export const loadArea = stub();

export const loadScript = stub();
Expand Down
14 changes: 11 additions & 3 deletions test/blocks/ost/mocks/ost-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ function getMetadata(name, doc = document) {
const loadScript = () => Promise.resolve();

const loadStyle = () => Promise.resolve();

const mockRes = ({ payload, status = 200 } = {}) => new Promise((resolve) => {
resolve({
status,
Expand All @@ -32,7 +31,6 @@ const mockRes = ({ payload, status = 200 } = {}) => new Promise((resolve) => {
text: () => payload,
});
});

function mockOstDeps({ failStatus = false, failMetadata = false, mockToken, overrideParams } = {}) {
const options = {
country: 'CH',
Expand Down Expand Up @@ -100,6 +98,16 @@ function unmockOstDeps() {
window.history.replaceState({}, '', ogUrl);
}

const customFetch = window.fetch;

export {
getConfig, getLocale, getMetadata, loadScript, loadStyle, mockOstDeps, unmockOstDeps, mockRes,
getConfig,
getLocale,
getMetadata,
loadScript,
loadStyle,
mockOstDeps,
unmockOstDeps,
mockRes,
customFetch,
};
2 changes: 1 addition & 1 deletion test/blocks/slideshare/mocks/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const getConfig = () => ({});
export const loadStyle = stub();

export const loadScript = stub();

export const customFetch = stub();
export const utf8ToB64 = (str) => window.btoa(unescape(encodeURIComponent(str)));

export function createIntersectionObserver({ el, callback /* , once = true, options = {} */ }) {
Expand Down
19 changes: 0 additions & 19 deletions test/utils/helpers.test.js

This file was deleted.

9 changes: 7 additions & 2 deletions test/utils/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ import { expect } from '@esm-bundle/chai';
import sinon from 'sinon';
import { waitFor, waitForElement } from '../helpers/waitfor.js';
import { mockFetch } from '../helpers/generalHelpers.js';
import { createTag } from '../../libs/utils/utils.js';
import { createTag, customFetch } from '../../libs/utils/utils.js';

const utils = {};

const config = {
codeRoot: '/libs',
locales: { '': { ietf: 'en-US', tk: 'hah7vzn.css' } },
Expand All @@ -31,6 +30,12 @@ describe('Utils', () => {
delete window.hlx;
});

it('fetches with cache param', async () => {
window.fetch = mockFetch({ payload: true });
const resp = await customFetch({ resource: './mocks/taxonomy.json', withCacheRules: true });
expect(resp.json()).to.be.true;
});

describe('with body', () => {
beforeEach(async () => {
window.fetch = mockFetch({ payload: { data: '' } });
Expand Down

0 comments on commit 3ad052c

Please sign in to comment.