Simulated throttling for soft navigation steps: land it upstream or bridge it in the package

Đang mở
#23 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

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ả

feature

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 devtools for 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 fetch is 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 on GoogleChrome/lighthouse.

  • Nobody has filed this upstream. lighthouse#14573 and lighthouse#17075 don't mention simulation; #17075 switches soft navigations from simulate to devtools.

To decide

1. How to land it upstream
  • Where to raise it: a new issue on GoogleChrome/lighthouse that 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 → devtools override 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

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. 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.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue tương tự

Thêm issue về TypeScript

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.