Simulated throttling for soft navigation steps: land it upstream or bridge it in the package
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 5/5
- Thời gian dự kiến
- Hơn một tuần
- Mức phù hợp với người mới
- 15/100
- Loại issue
- Tính năng
- Độ rõ ràng
- Cần làm rõ
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- devtools, typescript
- Lĩnh vực
- devtools, performance
Hướng nghiên cứu
Start with docs/simulated-throttling.md, then build the package (pnpm nx build lighthouse-soft-navigation) and run the two probe scripts in tmp/probe/ to reproduce the simulate-versus-devtools gap. The open work is a design decision (upstream issue versus package bridge options A-D), not a code change a newcomer can finish. Done means that decision is recorded and the accuracy bar is agreed.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Problem
A soft navigation step only works with throttlingMethod: 'devtools' or 'provided'. With simulate, which is Lighthouse's default and what PageSpeed Insights uses, seven audits error with rootRequest not found and the step has no Performance score.
That leaves a flow with two bad choices:
- set the throttling method on every soft navigation step, so page loads are simulated and soft navigations are measured under applied throttling, and the two don't compare; or
- set
devtoolsfor the whole flow, so page loads get slower, vary more and stop comparing with PageSpeed Insights.
What is known
Simulated throttling for soft navigations has the details. In short:
-
Lantern, the simulator, roots its graph at the document request. A soft navigation has none, so the graph is never built.
-
Giving the graph a root is not enough. On the e2e fixture app, with a synthetic root request:
FCP LCP Recorded without throttling 54–55 ms 206–207 ms DevTools throttling, same route 574–578 ms 1146–1151 ms Lantern with a synthetic root 9–10 ms 659–660 ms -
Three rules in Lantern cause that: the FCP graph only keeps render-blocking requests and a
fetchis never one, a request that waits for the response of another is simulated in parallel with it, and every origin is charged a new connection. -
Lantern's source is in the Chrome DevTools frontend (
front_end/models/trace/lantern) and reaches Lighthouse through@paulirish/trace_engine. Changes are reviewed on Chromium's Gerrit. Lantern issues are filed onGoogleChrome/lighthouse. -
Nobody has filed this upstream. lighthouse#14573 and lighthouse#17075 don't mention simulation; #17075 switches soft navigations from
simulatetodevtools.
To decide
1. How to land it upstream
- Where to raise it: a new issue on
GoogleChrome/lighthousethat references #14573 and #17075, or as part of the #17075 discussion. - Whether the Lighthouse team wants simulated soft navigations at all, or prefers the
simulate→devtoolsoverride for good. - The order of the changes. The doc lists seven. The graph root has to come first, and it can't ship alone because it turns the error into wrong numbers.
- Who collects the validation traces. Lighthouse's golden Lantern dataset (lighthouse#17411) has page loads only. Soft navigations need a flow per site: a URL and the interaction that navigates.
2. Whether the package bridges it until then
| Option | What the package does | Result | Cost |
|---|---|---|---|
| A. Nothing (today) | Requires devtools or provided, warns under simulate |
Correct numbers, mixed methods in one flow | None |
| B. Synthetic root | Extends the DevtoolsLog gatherer to add a cached document request at SoftNavigationStart |
Audits compute. FCP comes out at 10 ms on the fixture, where DevTools throttling measures 576 ms | Small, but not shippable alone. The request would also show up in network-requests |
| C. Own graph rules | B, plus the seven metric audits replaced for soft navigation steps by ones that pick their own FCP/LCP graphs and add the missing edges | Simulated numbers that no dataset has validated | A copy of Lantern's rules to maintain. Warm connections are inside ConnectionPool and can't be reached without patching it |
D. Observed under simulate |
Wraps the metric audits so a soft navigation step under simulate is computed as provided and warns |
Honest but unthrottled numbers, far lower than the simulated page loads next to them | Small. Scores look better than they are |
C and D are untested. B is what the measurement above used.
3. What "good enough" means for a bridge
If the package ships C, it needs an accuracy bar: which apps, compared against which reference (DevTools throttling is the obvious one), and how far off is acceptable.
Tasks
- Agree on raising it upstream, and where
- Prototype the graph root and the soft navigation paint graph against the fixture app
- Record soft navigations of a few real apps, without throttling and with DevTools throttling
- Decide between A, C and D for the package
Reproducing the measurement
Build the package (pnpm nx build lighthouse-soft-navigation), save both scripts in tmp/probe/ and run them from the workspace root. Lighthouse 13.6.0, @paulirish/trace_engine 0.0.67, Chrome 157.
tmp/probe/probe.mjs: runs the flow and saves its artifacts
// Probe: what happens to a soft navigation step under the default simulated throttling.
import { writeFileSync } from 'node:fs';
import { startFlow } from 'lighthouse';
import puppeteer from 'puppeteer';
import { softNavigationConfig } from '../../packages/lighthouse-soft-navigation/dist/index.js';
import { startAppServer } from '../../packages/lighthouse-soft-navigation-e2e/fixtures/app-server.mjs';
const out = new URL('.', import.meta.url).pathname;
const server = await startAppServer();
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
try {
console.log('chrome', await browser.version());
const page = await browser.newPage();
const flow = await startFlow(page, { config: softNavigationConfig() });
await flow.navigate(`${server.origin}/`);
// Soft navigation with the default throttling method (simulate).
await flow.navigate(async () => {
await page.click('#products-link');
});
// Soft navigation with DevTools throttling, for comparison.
await flow.navigate(
async () => {
await page.click('#home-link');
},
{ throttlingMethod: 'devtools' },
);
// The first soft navigation again, with DevTools throttling: the reference for the simulation.
await flow.navigate(
async () => {
await page.click('#products-link');
},
{ throttlingMethod: 'devtools' },
);
const result = await flow.createFlowResult();
writeFileSync(`${out}flow.json`, JSON.stringify(result));
writeFileSync(
`${out}artifacts.json`,
JSON.stringify(flow.createArtifactsJson()),
);
for (const [i, step] of result.steps.entries()) {
const { lhr } = step;
console.log(
`\n== step ${i}: ${step.name} | ${lhr.gatherMode} | ${lhr.configSettings.throttlingMethod} | perf ${lhr.categories.performance?.score}`,
);
for (const w of lhr.runWarnings) console.log(' warning:', w);
const errors = new Map();
for (const a of Object.values(lhr.audits)) {
if (a.scoreDisplayMode !== 'error') continue;
const list = errors.get(a.errorMessage) ?? [];
list.push(a.id);
errors.set(a.errorMessage, list);
}
for (const [message, ids] of errors)
console.log(` error "${message}": ${ids.join(', ')}`);
for (const id of [
'first-contentful-paint',
'largest-contentful-paint',
'total-blocking-time',
'speed-index',
'interactive',
]) {
const a = lhr.audits[id];
console.log(` ${id}: ${a?.scoreDisplayMode} ${a?.numericValue ?? ''}`);
}
}
} finally {
await browser.close();
await server.close();
}
tmp/probe/lantern-probe.mjs: runs Lantern on the saved step with a synthetic root
// Probe: what Lantern returns for a soft navigation once its graph has a root.
// The root is a synthetic, cached document request at SoftNavigationStart.
import { readFileSync } from 'node:fs';
import { LanternFirstContentfulPaint } from 'lighthouse/core/computed/metrics/lantern-first-contentful-paint.js';
import { LanternLargestContentfulPaint } from 'lighthouse/core/computed/metrics/lantern-largest-contentful-paint.js';
import { LanternInteractive } from 'lighthouse/core/computed/metrics/lantern-interactive.js';
import { LanternTotalBlockingTime } from 'lighthouse/core/computed/metrics/lantern-total-blocking-time.js';
import { LanternSpeedIndex } from 'lighthouse/core/computed/metrics/lantern-speed-index.js';
const dir = new URL('.', import.meta.url).pathname;
const { gatherSteps } = JSON.parse(readFileSync(`${dir}artifacts.json`, 'utf8'));
const flow = JSON.parse(readFileSync(`${dir}flow.json`, 'utf8'));
const stepIndex = Number(process.argv[2] ?? 1);
const { artifacts } = gatherSteps[stepIndex];
const start = artifacts.Trace.traceEvents.find(
(e) => e.name === 'SoftNavigationStart',
);
const frameId = artifacts.DevtoolsLog.find(
(e) => e.method === 'Network.requestWillBeSent',
).params.frameId;
const url = artifacts.URL.mainDocumentUrl;
const timestamp = start.ts / 1_000_000;
const requestId = 'synthetic-soft-navigation';
const synthetic = [
{
method: 'Network.requestWillBeSent',
params: {
requestId,
loaderId: requestId,
documentURL: url,
request: {
url,
method: 'GET',
headers: {},
initialPriority: 'VeryHigh',
mixedContentType: 'none',
referrerPolicy: 'strict-origin-when-cross-origin',
},
timestamp,
wallTime: 0,
initiator: { type: 'other' },
type: 'Document',
frameId,
},
},
{
method: 'Network.responseReceived',
params: {
requestId,
loaderId: requestId,
timestamp,
type: 'Document',
frameId,
response: {
url,
status: 200,
statusText: 'OK',
headers: {},
mimeType: 'text/html',
connectionReused: false,
connectionId: 0,
fromDiskCache: true,
encodedDataLength: 0,
protocol: 'http/1.1',
securityState: 'neutral',
},
},
},
{
method: 'Network.loadingFinished',
params: { requestId, timestamp, encodedDataLength: 0 },
},
];
const data = {
trace: artifacts.Trace,
devtoolsLog: process.env.NO_ROOT
? artifacts.DevtoolsLog
: [...synthetic, ...artifacts.DevtoolsLog],
gatherContext: artifacts.GatherContext,
settings: { ...artifacts.settings, throttlingMethod: 'simulate' },
URL: artifacts.URL,
SourceMaps: artifacts.SourceMaps,
HostDPR: artifacts.HostDPR,
simulator: null,
};
const context = { computedCache: new Map() };
const describe = (graph) => {
const nodes = [];
graph.traverse((node) => {
nodes.push(
node.type === 'network'
? `net ${new URL(node.request.url).pathname}${node.request.fromDiskCache ? ' (synthetic root)' : ''}`
: `cpu ${(node.duration / 1000).toFixed(1)}ms`,
);
});
return nodes;
};
const lhr = flow.steps[stepIndex].lhr;
console.log(`step ${stepIndex}: ${lhr.finalDisplayedUrl}`);
console.log(
'observed, unthrottled (ms from SoftNavigationStart):',
JSON.stringify(
Object.fromEntries(
['firstContentfulPaint', 'largestContentfulPaint'].map((k) => {
const name =
k === 'firstContentfulPaint'
? 'firstContentfulPaint'
: 'largestContentfulPaint::Candidate';
const events = artifacts.Trace.traceEvents.filter(
(e) => e.name === name,
);
return [k, (events.at(-1).ts - start.ts) / 1000];
}),
),
),
);
try {
const fcp = await LanternFirstContentfulPaint.request(data, context);
const lcp = await LanternLargestContentfulPaint.request(data, context);
const tti = await LanternInteractive.request(data, context);
const tbt = await LanternTotalBlockingTime.request(data, context);
const si = await LanternSpeedIndex.request(data, context);
for (const [name, m] of Object.entries({ fcp, lcp, tti, tbt, si })) {
console.log(
`${name}: ${m.timing.toFixed(1)} ms (optimistic ${m.optimisticEstimate.timeInMs.toFixed(1)}, pessimistic ${m.pessimisticEstimate.timeInMs.toFixed(1)})`,
);
}
console.log('fcp optimistic graph :', describe(fcp.optimisticGraph));
console.log('fcp pessimistic graph:', describe(fcp.pessimisticGraph));
console.log('lcp optimistic graph :', describe(lcp.optimisticGraph));
console.log('lcp pessimistic graph:', describe(lcp.pessimisticGraph));
for (const [node, t] of lcp.pessimisticEstimate.nodeTimings) {
console.log(
` lcp pessimistic timing: ${node.type === 'network' ? new URL(node.request.url).pathname : 'cpu'} ${t.startTime.toFixed(1)} → ${t.endTime.toFixed(1)}`,
);
}
} catch (error) {
console.log('error:', error.message);
}
node tmp/probe/probe.mjs # step 1 errors under simulate, step 3 is the DevTools reference
node tmp/probe/lantern-probe.mjs 1 # Lantern on step 1, with the synthetic root
NO_ROOT=1 node tmp/probe/lantern-probe.mjs 1 # without it: rootRequest not found
- Ngôn ngữ chính
- TypeScript
- Star
- 0
- Fork
- 0
- Merge trung bình
- 4 giờ 18 phút
- Pull request đã merge (30 ngày)
- 12
Chuẩn bị môi trường
Dự án này không cung cấp dev container, Dockerfile hay hướng dẫn đóng góp, nên bạn cần tự thiết lập môi trường: hãy bắt đầu từ README và xem hướng dẫn đóng góp lần đầu của chúng tôi để biết các bước chung.
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue tương tự
-
[analytics-core] getQueryParams does not decode `+` as space and truncates values containing `=`Đang mởbug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
amplitude/Amplitude-TypeScript#2179 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 2 ngày
-
ci good first issue severity: low
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
TMHSDigital/GH-Arcade#111 ·
-
AsnIntegerConverter.toASN coerces strings with +value, rounding integers outside safe-integer rangeĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
PeculiarVentures/asn1-schema#189 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
Maintainer thường phản hồi trong vòng 4 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100