From d74d998642fd70ccd51244a6069b72705bea7bfb Mon Sep 17 00:00:00 2001 From: Peter Hedenskog Date: Wed, 6 May 2026 07:34:35 +0200 Subject: [PATCH] Lift CDP renderBlockingStatus onto entries as _renderBlocking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chrome's CDP has carried the per-request render-blocking classification on Network.requestWillBeSent.params.renderBlockingStatus since Chrome 108, but chrome-har silently dropped it on the floor. Downstream HAR consumers (waterfall-tools' WPT-style renderer in particular) match _renderBlocking === 'blocking' to draw the orange ⊗ marker on each request that delays first paint, and without this field the marker never appears regardless of how blocking the CSS or script actually was. CDP emits the value in PascalCase ('Blocking', 'NonBlocking', 'InBodyParserBlocking', 'PotentiallyBlocking'); we lowercase before storing so the renderer's case-sensitive equality check works without each consumer having to re-normalise. Older Chrome builds that don't emit the field leave _renderBlocking unset, distinguishing "not blocking" from "browser couldn't tell us" — the same tri-state convention used elsewhere in the project. Co-authored-by: Claude Opus 4.7 (1M context) noreply@anthropic.com --- index.js | 10 ++++ test/tests.js | 140 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+) diff --git a/index.js b/index.js index 0dcf769..6c47a2b 100644 --- a/index.js +++ b/index.js @@ -226,6 +226,16 @@ export function harFromMessages(messages, options) { _resourceType: params.type ? params.type.toLowerCase() : undefined }; + // CDP `renderBlockingStatus` (Chrome 108+) — values are PascalCase + // (`Blocking`, `NonBlocking`, `InBodyParserBlocking`, `Potentially…`). + // waterfall-tools' WPT-style renderer matches `_renderBlocking === + // 'blocking'` to draw the orange ⊗ marker, so lowercase the value + // here. Other variants ride along verbatim-lowercased for any + // consumer that wants to inspect them. + if (params.renderBlockingStatus) { + entry._renderBlocking = params.renderBlockingStatus.toLowerCase(); + } + // The object initiator change according to its type switch (params.initiator.type) { case 'parser': { diff --git a/test/tests.js b/test/tests.js index af00105..e61039d 100644 --- a/test/tests.js +++ b/test/tests.js @@ -130,6 +130,146 @@ test('Parses IPv6 address', t => { ); }); +test('Lifts CDP renderBlockingStatus onto entries as `_renderBlocking`', t => { + // Minimal synthetic CDP stream: one render-blocking document + one + // non-blocking subresource. The renderer in waterfall-tools matches + // `_renderBlocking === 'blocking'` (lowercase) to draw the orange ⊗ + // marker, so we assert the casing here too. + const frameId = 'F1'; + const baseMessages = (requestId, url, renderBlockingStatus) => [ + { + method: 'Network.requestWillBeSent', + params: { + requestId, + frameId, + loaderId: 'L1', + documentURL: 'https://example.com/', + request: { + url, + method: 'GET', + headers: {}, + initialPriority: 'High' + }, + timestamp: 1, + wallTime: 1_700_000_000, + initiator: { type: 'other' }, + type: requestId === '1' ? 'Document' : 'Script', + ...(renderBlockingStatus ? { renderBlockingStatus } : {}) + } + }, + { + method: 'Network.responseReceived', + params: { + requestId, + frameId, + loaderId: 'L1', + timestamp: 2, + type: requestId === '1' ? 'Document' : 'Script', + response: { + url, + status: 200, + statusText: 'OK', + headers: { 'content-type': 'text/html' }, + mimeType: 'text/html', + fromDiskCache: false, + fromServiceWorker: false, + encodedDataLength: 100, + protocol: 'http/1.1', + connectionId: 1, + remoteIPAddress: '127.0.0.1', + timing: { + requestTime: 1, + sendStart: 0, + sendEnd: 1, + receiveHeadersEnd: 2 + } + } + } + }, + { + method: 'Network.loadingFinished', + params: { requestId, timestamp: 3, encodedDataLength: 100 } + } + ]; + const messages = [ + { + method: 'Page.frameStartedLoading', + params: { frameId } + }, + ...baseMessages('1', 'https://example.com/', 'Blocking'), + ...baseMessages('2', 'https://example.com/app.js', 'NonBlocking') + ]; + const har = harFromMessages(messages); + const byUrl = Object.fromEntries( + har.log.entries.map(e => [e.request.url, e]) + ); + t.is(byUrl['https://example.com/']._renderBlocking, 'blocking'); + t.is(byUrl['https://example.com/app.js']._renderBlocking, 'nonblocking'); +}); + +test('Omits `_renderBlocking` when CDP did not report it', t => { + // Older Chrome builds don't emit renderBlockingStatus at all — make sure + // we don't materialise the field as `undefined` or an empty string in + // that case. Absent input → absent output. + const messages = [ + { method: 'Page.frameStartedLoading', params: { frameId: 'F1' } }, + { + method: 'Network.requestWillBeSent', + params: { + requestId: '1', + frameId: 'F1', + loaderId: 'L1', + documentURL: 'https://example.com/', + request: { + url: 'https://example.com/', + method: 'GET', + headers: {}, + initialPriority: 'High' + }, + timestamp: 1, + wallTime: 1_700_000_000, + initiator: { type: 'other' }, + type: 'Document' + } + }, + { + method: 'Network.responseReceived', + params: { + requestId: '1', + frameId: 'F1', + loaderId: 'L1', + timestamp: 2, + type: 'Document', + response: { + url: 'https://example.com/', + status: 200, + statusText: 'OK', + headers: { 'content-type': 'text/html' }, + mimeType: 'text/html', + fromDiskCache: false, + fromServiceWorker: false, + encodedDataLength: 100, + protocol: 'http/1.1', + connectionId: 1, + remoteIPAddress: '127.0.0.1', + timing: { + requestTime: 1, + sendStart: 0, + sendEnd: 1, + receiveHeadersEnd: 2 + } + } + } + }, + { + method: 'Network.loadingFinished', + params: { requestId: '1', timestamp: 3, encodedDataLength: 100 } + } + ]; + const har = harFromMessages(messages); + t.false('_renderBlocking' in har.log.entries[0]); +}); + test('Forwards the resource type value', t => { const perflogPath = perflog('www.google.ru.json'); const expected = {