From f79a1d7016c977e753a8da815e5d4adf7ec4c8c5 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Tue, 28 Nov 2023 17:44:19 -0800 Subject: [PATCH 1/7] core(legacy-javascript): exclude header size for estimating wasted bytes --- .../byte-efficiency/byte-efficiency-audit.js | 60 +++++++++++++++++++ .../byte-efficiency/legacy-javascript.js | 41 +++++++------ core/lib/network-request.js | 11 ++++ .../byte-efficiency-audit-test.js | 51 ++++++++++++++++ .../byte-efficiency/legacy-javascript-test.js | 1 + 5 files changed, 146 insertions(+), 18 deletions(-) diff --git a/core/audits/byte-efficiency/byte-efficiency-audit.js b/core/audits/byte-efficiency/byte-efficiency-audit.js index f7d2219fdf4b..baff738c70b6 100644 --- a/core/audits/byte-efficiency/byte-efficiency-audit.js +++ b/core/audits/byte-efficiency/byte-efficiency-audit.js @@ -13,6 +13,7 @@ import {PageDependencyGraph} from '../../computed/page-dependency-graph.js'; import {LanternLargestContentfulPaint} from '../../computed/metrics/lantern-largest-contentful-paint.js'; import {LanternFirstContentfulPaint} from '../../computed/metrics/lantern-first-contentful-paint.js'; import {LCPImageRecord} from '../../computed/lcp-image-record.js'; +import {NetworkRequest} from '../../lib/network-request.js'; const str_ = i18n.createIcuMessageFn(import.meta.url, {}); @@ -100,6 +101,65 @@ class ByteEfficiencyAudit extends Audit { } } + /** + * Estimates the number of bytes the content of this network record would have consumed on the network based on the + * uncompressed size (totalBytes). Uses the actual transfer size from the network record if applicable, + * minus the size of the response headers. + * + * This differs from `estimateTransferSize` only in that is subtracts the response headers from the estimate. + * + * @param {LH.Artifacts.NetworkRequest|undefined} networkRecord + * @param {number} totalBytes Uncompressed size of the resource + * @param {LH.Crdp.Network.ResourceType=} resourceType + * @return {number} + */ + static estimateCompressedContentSize(networkRecord, totalBytes, resourceType) { + if (!networkRecord) { + // We don't know how many bytes this asset used on the network, but we can guess it was + // roughly the size of the content gzipped. + // See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/optimize-encoding-and-transfer for specific CSS/Script examples + // See https://discuss.httparchive.org/t/file-size-and-compression-savings/145 for fallback multipliers + switch (resourceType) { + case 'Stylesheet': + // Stylesheets tend to compress extremely well. + return Math.round(totalBytes * 0.2); + case 'Script': + case 'Document': + // Scripts and HTML compress fairly well too. + return Math.round(totalBytes * 0.33); + default: + // Otherwise we'll just fallback to the average savings in HTTPArchive + return Math.round(totalBytes * 0.5); + } + } + + // Get the size of the response body on the network. + let contentTransferSize = networkRecord.transferSize || 0; + if (!NetworkRequest.isContentEncoded(networkRecord)) { + // This is not encoded, so we can use resourceSize directly. + // This would be equivalent to transfer size minus headers transfer size, but transfer size + // may also include bytes for SSL connection etc. + contentTransferSize = networkRecord.resourceSize; + } else if (networkRecord.responseHeadersTransferSize) { + // Subtract the size of the encoded headers. + contentTransferSize = + Math.max(0, contentTransferSize - networkRecord.responseHeadersTransferSize); + } + + if (networkRecord.resourceType === resourceType) { + // This was a regular standalone asset, just use the transfer size. + return contentTransferSize; + } else { + // This was an asset that was inlined in a different resource type (e.g. HTML document). + // Use the compression ratio of the resource to estimate the total transferred bytes. + const resourceSize = networkRecord.resourceSize || 0; + // Get the compression ratio, if it's an invalid number, assume no compression. + const compressionRatio = Number.isFinite(resourceSize) && resourceSize > 0 ? + (contentTransferSize / resourceSize) : 1; + return Math.round(totalBytes * compressionRatio); + } + } + /** * @param {LH.Artifacts} artifacts * @param {LH.Audit.Context} context diff --git a/core/audits/byte-efficiency/legacy-javascript.js b/core/audits/byte-efficiency/legacy-javascript.js index d27ad065f7ae..69144101b499 100644 --- a/core/audits/byte-efficiency/legacy-javascript.js +++ b/core/audits/byte-efficiency/legacy-javascript.js @@ -391,34 +391,39 @@ class LegacyJavascript extends ByteEfficiencyAudit { } /** - * Utility function to estimate transfer size and cache calculation. + * Utility function to estimate the ratio of the compression on the resource. + * This excludes the size of the response headers. + * Also caches the calculation. * * Note: duplicated-javascript does this exact thing. In the future, consider - * making a generic estimator on ByteEfficienyAudit. - * @param {Map} transferRatioByUrl + * making a generic estimator on ByteEfficiencyAudit. + * @param {Map} compressionRatioByUrl * @param {string} url * @param {LH.Artifacts} artifacts * @param {Array} networkRecords */ - static async estimateTransferRatioForScript(transferRatioByUrl, url, artifacts, networkRecords) { - let transferRatio = transferRatioByUrl.get(url); - if (transferRatio !== undefined) return transferRatio; + static async estimateCompressionRatioForContent(compressionRatioByUrl, url, + artifacts, networkRecords) { + let compressionRatio = compressionRatioByUrl.get(url); + if (compressionRatio !== undefined) return compressionRatio; const script = artifacts.Scripts.find(script => script.url === url); - if (!script || script.content === null) { + if (!script) { // Can't find content, so just use 1. - transferRatio = 1; + compressionRatio = 1; } else { const networkRecord = getRequestForScript(networkRecords, script); - const contentLength = script.length || 0; - const transferSize = - ByteEfficiencyAudit.estimateTransferSize(networkRecord, contentLength, 'Script'); - transferRatio = transferSize / contentLength; + const contentLength = networkRecord?.resourceSize ? + networkRecord.resourceSize : + script.length || 0; + const compressedSize = + ByteEfficiencyAudit.estimateCompressedContentSize(networkRecord, contentLength, 'Script'); + compressionRatio = compressedSize / contentLength; } - transferRatioByUrl.set(url, transferRatio); - return transferRatio; + compressionRatioByUrl.set(url, compressionRatio); + return compressionRatio; } /** @@ -443,14 +448,14 @@ class LegacyJavascript extends ByteEfficiencyAudit { ]); /** @type {Map} */ - const transferRatioByUrl = new Map(); + const compressionRatioByUrl = new Map(); const scriptToMatchResults = this.detectAcrossScripts(matcher, artifacts.Scripts, networkRecords, bundles); for (const [script, matches] of scriptToMatchResults.entries()) { - const transferRatio = await this.estimateTransferRatioForScript( - transferRatioByUrl, script.url, artifacts, networkRecords); - const wastedBytes = Math.round(this.estimateWastedBytes(matches) * transferRatio); + const compressionRatio = await this.estimateCompressionRatioForContent( + compressionRatioByUrl, script.url, artifacts, networkRecords); + const wastedBytes = Math.round(this.estimateWastedBytes(matches) * compressionRatio); /** @type {typeof items[number]} */ const item = { url: script.url, diff --git a/core/lib/network-request.js b/core/lib/network-request.js index 133e3e482e35..10797559789e 100644 --- a/core/lib/network-request.js +++ b/core/lib/network-request.js @@ -135,6 +135,7 @@ class NetworkRequest { // Go read the comment on _updateTransferSizeForLightrider. this.transferSize = 0; + this.responseHeadersTransferSize = 0; this.resourceSize = 0; this.fromDiskCache = false; this.fromMemoryCache = false; @@ -344,6 +345,7 @@ class NetworkRequest { this.responseHeadersEndTime = timestamp * 1000; this.transferSize = response.encodedDataLength; + this.responseHeadersTransferSize = response.encodedDataLength; if (typeof response.fromDiskCache === 'boolean') this.fromDiskCache = response.fromDiskCache; if (typeof response.fromPrefetchCache === 'boolean') { this.fromPrefetchCache = response.fromPrefetchCache; @@ -600,6 +602,15 @@ class NetworkRequest { return reason === 'HSTS' && NetworkRequest.isSecureRequest(destination); } + /** + * Returns whether the network request was sent encoded. + * @param {NetworkRequest} record + * @return {boolean} + */ + static isContentEncoded(record) { + return record.responseHeaders.some(item => item.name === 'Content-Encoding'); + } + /** * Resource size is almost always the right one to be using because of the below: * `transferSize = resourceSize + headers.length`. diff --git a/core/test/audits/byte-efficiency/byte-efficiency-audit-test.js b/core/test/audits/byte-efficiency/byte-efficiency-audit-test.js index 5d4f5a256404..1ebd9f56c812 100644 --- a/core/test/audits/byte-efficiency/byte-efficiency-audit-test.js +++ b/core/test/audits/byte-efficiency/byte-efficiency-audit-test.js @@ -123,6 +123,57 @@ describe('Byte efficiency base audit', () => { }); }); + describe('#estimateCompressedContentSize', () => { + const estimate = ByteEfficiencyAudit.estimateCompressedContentSize; + const encoding = [{name: 'Content-Encoding'}]; + + it('should estimate by resource type compression ratio when no network info available', () => { + assert.equal(estimate(undefined, 1000, 'Stylesheet'), 200); + assert.equal(estimate(undefined, 1000, 'Script'), 330); + assert.equal(estimate(undefined, 1000, 'Document'), 330); + assert.equal(estimate(undefined, 1000, ''), 500); + }); + + it('should return transferSize when asset matches and is encoded', () => { + const resourceType = 'Stylesheet'; + const result = estimate( + {transferSize: 1234, resourceType, responseHeaders: encoding}, + 10000, 'Stylesheet'); + assert.equal(result, 1234); + }); + + it('should return resourceSize when asset matches and is not encoded', () => { + const resourceType = 'Stylesheet'; + const result = estimate( + {transferSize: 1235, resourceSize: 1234, resourceType, responseHeaders: []}, + 10000, 'Stylesheet'); + assert.equal(result, 1234); + }); + + // Ex: JS script embedded in HTML response. + it('should estimate by network compression ratio when asset does not match', () => { + const resourceType = 'Other'; + const result = estimate( + {resourceSize: 2000, transferSize: 1000, resourceType, responseHeaders: encoding}, + 100); + assert.equal(result, 50); + }); + + it('should not error when missing resource size', () => { + const resourceType = 'Other'; + const result = estimate({transferSize: 1000, resourceType, responseHeaders: []}, 100); + assert.equal(result, 100); + }); + + it('should not error when resource size is 0', () => { + const resourceType = 'Other'; + const result = estimate( + {transferSize: 1000, resourceSize: 0, resourceType, responseHeaders: []}, + 100); + assert.equal(result, 100); + }); + }); + it('should format details', async () => { const result = await ByteEfficiencyAudit.createAuditProduct({ headings: baseHeadings, diff --git a/core/test/audits/byte-efficiency/legacy-javascript-test.js b/core/test/audits/byte-efficiency/legacy-javascript-test.js index d5129f53f364..c1d7d8259fb2 100644 --- a/core/test/audits/byte-efficiency/legacy-javascript-test.js +++ b/core/test/audits/byte-efficiency/legacy-javascript-test.js @@ -19,6 +19,7 @@ const getResult = scripts => { ...scripts.map(({url}, index) => ({ requestId: String(index), url, + responseHeaders: [], })), ]; const artifacts = { From 8ffd34f7afd7a04e761711d3cda5cc63e46b97c5 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Tue, 28 Nov 2023 19:02:19 -0800 Subject: [PATCH 2/7] fix roundtrip test --- core/test/network-records-to-devtools-log-test.js | 7 ++++++- core/test/network-records-to-devtools-log.js | 3 +-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/core/test/network-records-to-devtools-log-test.js b/core/test/network-records-to-devtools-log-test.js index 167b0e22bd68..61d2f4e88b10 100644 --- a/core/test/network-records-to-devtools-log-test.js +++ b/core/test/network-records-to-devtools-log-test.js @@ -57,7 +57,12 @@ describe('networkRecordsToDevtoolsLog', () => { const roundTripLogs = networkRecordsToDevtoolsLog(records, {skipVerification: true}); const roundTripRecords = NetworkRecorder.recordsFromLogs(roundTripLogs); - expect(roundTripRecords).toEqual(records); + // First compare element-wise, as doing all at once results in too verbose an error message. + const len = Math.min(roundTripRecords.length, records.length); + for (let i = 0; i < len; i++) { + expect(roundTripRecords[i]).toEqual(records[i]); + } + expect(roundTripRecords.length).toEqual(records.length); }); it('should roundtrip fake network records multiple times', () => { diff --git a/core/test/network-records-to-devtools-log.js b/core/test/network-records-to-devtools-log.js index 77c0e1c83376..207787172c71 100644 --- a/core/test/network-records-to-devtools-log.js +++ b/core/test/network-records-to-devtools-log.js @@ -288,8 +288,7 @@ function getResponseReceivedEvent(networkRecord, index, normalizedTiming) { connectionId: networkRecord.connectionId || 140, fromDiskCache: networkRecord.fromDiskCache || false, fromServiceWorker: networkRecord.fetchedViaServiceWorker || false, - encodedDataLength: networkRecord.transferSize === undefined ? - 0 : networkRecord.transferSize, + encodedDataLength: networkRecord.responseHeadersTransferSize, timing: {...normalizedTiming.timing}, protocol: networkRecord.protocol || 'http/1.1', }, From 5a626279e5c79c9783e7237d5ee668ab0f6c9c37 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Fri, 1 Dec 2023 14:22:02 -0800 Subject: [PATCH 3/7] update sample json, and fix network record roundtrip test --- .../metrics/time-to-first-byte-test.js | 2 ++ .../user-flows/reports/sample-flow-result.json | 18 ++++++++++++------ core/test/network-records-to-devtools-log.js | 2 +- core/test/results/sample_v2.json | 6 +++--- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/core/test/computed/metrics/time-to-first-byte-test.js b/core/test/computed/metrics/time-to-first-byte-test.js index 39dc6539f86a..152cb0b18503 100644 --- a/core/test/computed/metrics/time-to-first-byte-test.js +++ b/core/test/computed/metrics/time-to-first-byte-test.js @@ -44,6 +44,7 @@ function mockNetworkRecords() { transferSize: 300, url: requestedUrl, frameId: 'ROOT_FRAME', + responseHeaders: [{name: 'Content-Encoding'}], }, { requestId: '2:redirect', @@ -57,6 +58,7 @@ function mockNetworkRecords() { transferSize: 16_000, url: mainDocumentUrl, frameId: 'ROOT_FRAME', + responseHeaders: [{name: 'Content-Encoding'}], }]; } diff --git a/core/test/fixtures/user-flows/reports/sample-flow-result.json b/core/test/fixtures/user-flows/reports/sample-flow-result.json index 07bcf2acc2af..b6cca67e36d6 100644 --- a/core/test/fixtures/user-flows/reports/sample-flow-result.json +++ b/core/test/fixtures/user-flows/reports/sample-flow-result.json @@ -3286,7 +3286,7 @@ "items": [ { "url": "https://www.mikescerealshack.co/_next/static/chunks/commons.49455e4fa8cc3f51203f.js", - "wastedBytes": 57, + "wastedBytes": 167, "subItems": { "type": "subitems", "items": [ @@ -3306,7 +3306,7 @@ } ], "overallSavingsMs": 0, - "overallSavingsBytes": 57, + "overallSavingsBytes": 167, "sortedBy": [ "wastedBytes" ], @@ -8034,7 +8034,7 @@ "core/lib/i18n/i18n.js | displayValueByteSavings": [ { "values": { - "wastedBytes": 57 + "wastedBytes": 167 }, "path": "audits[legacy-javascript].displayValue" } @@ -20850,7 +20850,7 @@ "scoreDisplayMode": "metricSavings", "numericValue": 0, "numericUnit": "millisecond", - "displayValue": "", + "displayValue": "Potential savings of 0 KiB", "metricSavings": { "FCP": 0, "LCP": 0 @@ -20884,7 +20884,7 @@ "items": [ { "url": "https://www.mikescerealshack.co/_next/static/chunks/commons.49455e4fa8cc3f51203f.js", - "wastedBytes": 0, + "wastedBytes": 167, "subItems": { "type": "subitems", "items": [ @@ -20904,7 +20904,7 @@ } ], "overallSavingsMs": 0, - "overallSavingsBytes": 0, + "overallSavingsBytes": 167, "sortedBy": [ "wastedBytes" ], @@ -25629,6 +25629,12 @@ "wastedBytes": 52102 }, "path": "audits[uses-responsive-images].displayValue" + }, + { + "values": { + "wastedBytes": 167 + }, + "path": "audits[legacy-javascript].displayValue" } ], "core/lib/i18n/i18n.js | columnResourceSize": [ diff --git a/core/test/network-records-to-devtools-log.js b/core/test/network-records-to-devtools-log.js index 207787172c71..46ed23c29bbc 100644 --- a/core/test/network-records-to-devtools-log.js +++ b/core/test/network-records-to-devtools-log.js @@ -288,7 +288,7 @@ function getResponseReceivedEvent(networkRecord, index, normalizedTiming) { connectionId: networkRecord.connectionId || 140, fromDiskCache: networkRecord.fromDiskCache || false, fromServiceWorker: networkRecord.fetchedViaServiceWorker || false, - encodedDataLength: networkRecord.responseHeadersTransferSize, + encodedDataLength: networkRecord.responseHeadersTransferSize || networkRecord.transferSize, timing: {...normalizedTiming.timing}, protocol: networkRecord.protocol || 'http/1.1', }, diff --git a/core/test/results/sample_v2.json b/core/test/results/sample_v2.json index a9854feef096..d620125a71f1 100644 --- a/core/test/results/sample_v2.json +++ b/core/test/results/sample_v2.json @@ -4975,7 +4975,7 @@ "items": [ { "url": "http://localhost:10200/dobetterweb/third_party/aggressive-promise-polyfill.js", - "wastedBytes": 26622, + "wastedBytes": 26585, "subItems": { "type": "subitems", "items": [ @@ -5005,7 +5005,7 @@ } ], "overallSavingsMs": 450, - "overallSavingsBytes": 26622, + "overallSavingsBytes": 26585, "sortedBy": [ "wastedBytes" ], @@ -10473,7 +10473,7 @@ }, { "values": { - "wastedBytes": 26622 + "wastedBytes": 26585 }, "path": "audits[legacy-javascript].displayValue" } From 65e104e57bc8d0f36946a3eb05ba35c549f7deac Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Fri, 1 Dec 2023 14:56:31 -0800 Subject: [PATCH 4/7] fix leg js test --- core/scripts/legacy-javascript/run.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/scripts/legacy-javascript/run.js b/core/scripts/legacy-javascript/run.js index 5b591948e0ba..ceb4b7074474 100644 --- a/core/scripts/legacy-javascript/run.js +++ b/core/scripts/legacy-javascript/run.js @@ -137,9 +137,10 @@ function getLegacyJavascriptResults(code, map, {sourceMaps}) { const documentUrl = 'http://localhost/index.html'; // These URLs don't matter. const scriptUrl = 'https://localhost/main.bundle.min.js'; const scriptId = '10001'; + const responseHeaders = [{name: 'Content-Encoding', value: 'gzip'}]; const networkRecords = [ - {url: documentUrl, requestId: '1000.1', resourceType: /** @type {const} */ ('Document')}, - {url: scriptUrl, requestId: '1000.2'}, + {url: documentUrl, requestId: '1000.1', resourceType: /** @type {const} */ ('Document'), responseHeaders}, + {url: scriptUrl, requestId: '1000.2', responseHeaders}, ]; const devtoolsLogs = networkRecordsToDevtoolsLog(networkRecords); From 0ecf21fc817ffb86f8887ede9e589cbc4b1bbb2f Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Fri, 1 Dec 2023 14:58:02 -0800 Subject: [PATCH 5/7] Update core/audits/byte-efficiency/legacy-javascript.js Co-authored-by: Adam Raine <6752989+adamraine@users.noreply.github.com> --- core/audits/byte-efficiency/legacy-javascript.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/audits/byte-efficiency/legacy-javascript.js b/core/audits/byte-efficiency/legacy-javascript.js index 69144101b499..b0e394ab6405 100644 --- a/core/audits/byte-efficiency/legacy-javascript.js +++ b/core/audits/byte-efficiency/legacy-javascript.js @@ -414,9 +414,7 @@ class LegacyJavascript extends ByteEfficiencyAudit { compressionRatio = 1; } else { const networkRecord = getRequestForScript(networkRecords, script); - const contentLength = networkRecord?.resourceSize ? - networkRecord.resourceSize : - script.length || 0; + const contentLength = networkRecord?.resourceSize || script.length || 0; const compressedSize = ByteEfficiencyAudit.estimateCompressedContentSize(networkRecord, contentLength, 'Script'); compressionRatio = compressedSize / contentLength; From 14b4a7e110a64fe68e375859d38cc6033bc0dfb2 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Fri, 1 Dec 2023 15:12:14 -0800 Subject: [PATCH 6/7] fix weird stack overflow bug --- core/test/network-records-to-devtools-log.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/test/network-records-to-devtools-log.js b/core/test/network-records-to-devtools-log.js index f3d15e0c0ab8..aee9e4158a3b 100644 --- a/core/test/network-records-to-devtools-log.js +++ b/core/test/network-records-to-devtools-log.js @@ -288,7 +288,7 @@ function getResponseReceivedEvent(networkRecord, index, normalizedTiming) { connectionId: networkRecord.connectionId || 140, fromDiskCache: networkRecord.fromDiskCache || false, fromServiceWorker: networkRecord.fetchedViaServiceWorker || false, - encodedDataLength: networkRecord.responseHeadersTransferSize || networkRecord.transferSize, + encodedDataLength: networkRecord.responseHeadersTransferSize || networkRecord.transferSize || 0, timing: {...normalizedTiming.timing}, protocol: networkRecord.protocol || 'http/1.1', }, From 718090c65f425afea6ef7ccc781910747e2a0a66 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Fri, 1 Dec 2023 15:36:13 -0800 Subject: [PATCH 7/7] lint --- core/scripts/legacy-javascript/run.js | 3 ++- core/test/network-records-to-devtools-log.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/scripts/legacy-javascript/run.js b/core/scripts/legacy-javascript/run.js index ceb4b7074474..f769bb7c7a94 100644 --- a/core/scripts/legacy-javascript/run.js +++ b/core/scripts/legacy-javascript/run.js @@ -139,7 +139,8 @@ function getLegacyJavascriptResults(code, map, {sourceMaps}) { const scriptId = '10001'; const responseHeaders = [{name: 'Content-Encoding', value: 'gzip'}]; const networkRecords = [ - {url: documentUrl, requestId: '1000.1', resourceType: /** @type {const} */ ('Document'), responseHeaders}, + {url: documentUrl, requestId: '1000.1', resourceType: /** @type {const} */ ('Document'), + responseHeaders}, {url: scriptUrl, requestId: '1000.2', responseHeaders}, ]; const devtoolsLogs = networkRecordsToDevtoolsLog(networkRecords); diff --git a/core/test/network-records-to-devtools-log.js b/core/test/network-records-to-devtools-log.js index aee9e4158a3b..6948ca7c90af 100644 --- a/core/test/network-records-to-devtools-log.js +++ b/core/test/network-records-to-devtools-log.js @@ -288,7 +288,8 @@ function getResponseReceivedEvent(networkRecord, index, normalizedTiming) { connectionId: networkRecord.connectionId || 140, fromDiskCache: networkRecord.fromDiskCache || false, fromServiceWorker: networkRecord.fetchedViaServiceWorker || false, - encodedDataLength: networkRecord.responseHeadersTransferSize || networkRecord.transferSize || 0, + encodedDataLength: + networkRecord.responseHeadersTransferSize || networkRecord.transferSize || 0, timing: {...normalizedTiming.timing}, protocol: networkRecord.protocol || 'http/1.1', },