// Page weight and search measurements in a real browser.
// node measure.mjs weight ...
// node measure.mjs search starlight|mkdocs ...
// Transfer sizes are bytes on the wire (gzip from serve.mjs), by type.
// playwright-core is resolved from website/node_modules, above this file,
// wherever the command is run from. It is not a dependency of the build:
// npm install --no-save --prefix website playwright-core
const { chromium } = await import('playwright-core').catch((error) => {
console.error(`playwright-core is not in website/node_modules (${error.code}): `
+ 'npm install --no-save --prefix website playwright-core');
process.exit(2);
});
const [mode, base, ...rest] = process.argv.slice(2);
const browser = await chromium.launch({ executablePath: process.env.CHROME_PATH || '/usr/bin/google-chrome' });
const track = (page) => {
const rows = [];
page.on('response', async (response) => {
const request = response.request();
const headers = await response.allHeaders();
let bytes = Number(headers['content-length'] ?? 0);
if (!bytes) {
try { bytes = (await response.body()).length; } catch { bytes = 0; }
}
rows.push({ url: response.url(), type: request.resourceType(), bytes });
});
return rows;
};
const sum = (rows, test) => rows.filter(test).reduce((total, row) => total + row.bytes, 0);
if (mode === 'weight') {
for (const path of rest) {
const context = await browser.newContext({ viewport: { width: 390, height: 844 } });
const page = await context.newPage();
const rows = track(page);
await page.goto(new URL(path, base).href, { waitUntil: 'networkidle' });
await page.waitForTimeout(500);
const dom = await page.evaluate(() => document.getElementsByTagName('*').length);
const kb = (n) => (n / 1024).toFixed(1);
console.log(JSON.stringify({
page: path,
html_kb: kb(sum(rows, (r) => r.type === 'document')),
js_kb: kb(sum(rows, (r) => r.type === 'script')),
css_kb: kb(sum(rows, (r) => r.type === 'stylesheet')),
other_kb: kb(sum(rows, (r) => !['document', 'script', 'stylesheet'].includes(r.type))),
total_kb: kb(sum(rows, () => true)),
requests: rows.length,
dom_nodes: dom,
}));
await context.close();
}
} else if (mode === 'search') {
const [engine, ...queries] = rest;
const context = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await context.newPage();
const rows = track(page);
await page.goto(base, { waitUntil: 'networkidle' });
await page.waitForTimeout(500);
const before = rows.length;
const first = [];
for (const [n, query] of queries.entries()) {
const mark = rows.length;
let results;
if (engine === 'starlight-ui') {
// The reader's path: open the dialog, type, wait for the result list.
if (n === 0) await page.click('button[data-open-modal]');
const input = page.locator('dialog input[type="text"], dialog input[type="search"]').first();
await input.fill(query);
await page.waitForSelector('.pagefind-ui__result', { timeout: 15000 });
await page.waitForTimeout(1200);
results = await page.evaluate(() => {
const items = [...document.querySelectorAll('.pagefind-ui__result')];
return {
count: document.querySelector('.pagefind-ui__message')?.textContent ?? '',
top: items.slice(0, 5).map((r) => `${r.querySelector('.pagefind-ui__result-link')?.textContent?.trim()} | ${r.querySelector('.pagefind-ui__result-link')?.getAttribute('href')}`),
};
});
} else if (engine === 'starlight') {
results = await page.evaluate(async ({ query, root, ranking }) => {
if (!window.__pf) {
window.__pf = await import(`${root}pagefind/pagefind.js`);
if (ranking) await window.__pf.options({ ranking });
}
const search = await window.__pf.search(query);
const top = await Promise.all(search.results.slice(0, 5).map((r) => r.data()));
return { count: search.results.length, top: top.map((d) => `${d.meta.title} | ${d.url}`) };
}, { query, root: new URL('/retrobios/', base).href, ranking: process.env.RB_RANKING ? JSON.parse(process.env.RB_RANKING) : null });
} else {
// Material: open the search through its share link (?q=) and read the result list.
await page.goto(`${base}?q=${encodeURIComponent(query)}`, { waitUntil: 'networkidle' });
await page.waitForFunction(
(q) => document.querySelector('input[data-md-component="search-query"]')?.value === q
&& /matching|No matching/.test(document.querySelector('.md-search-result__meta')?.textContent ?? ''),
query, { timeout: 30000 },
);
await page.waitForTimeout(800);
results = await page.evaluate(() => {
const items = [...document.querySelectorAll('.md-search-result__item > a')];
const meta = document.querySelector('.md-search-result__meta')?.textContent ?? '';
return { count: meta, top: items.slice(0, 5).map((a) => `${a.querySelector('h1,h2')?.textContent?.trim()} | ${a.getAttribute('href')}`) };
});
}
await page.waitForTimeout(300);
const transfer = sum(rows.slice(mark), () => true);
if (n === 0) first.push(...rows.slice(before));
console.log(JSON.stringify({ query, transfer_kb: (transfer / 1024).toFixed(1), ...results }));
}
console.log(JSON.stringify({ first_search_kb: (sum(first, () => true) / 1024).toFixed(1), first_search_requests: first.length }));
await context.close();
}
await browser.close();