-
Notifications
You must be signed in to change notification settings - Fork 9.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
core: merge render blocking audits to lantern #4995
Changes from all commits
29b3409
7f449c1
c2b340c
e08bb88
f5d3b65
6d53604
2e88795
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
/** | ||
* @license Copyright 2018 Google Inc. All Rights Reserved. | ||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. | ||
*/ | ||
|
||
/** | ||
* @fileoverview Audit a page to see if it does have resources that are blocking first paint | ||
*/ | ||
|
||
'use strict'; | ||
|
||
const Audit = require('../audit'); | ||
const Node = require('../../lib/dependency-graph/node'); | ||
const ByteEfficiencyAudit = require('./byte-efficiency-audit'); | ||
const UnusedCSS = require('./unused-css-rules'); | ||
const WebInspector = require('../../lib/web-inspector'); | ||
|
||
// Because of the way we detect blocking stylesheets, asynchronously loaded | ||
// CSS with link[rel=preload] and an onload handler (see https://github.com/filamentgroup/loadCSS) | ||
// can be falsely flagged as blocking. Therefore, ignore stylesheets that loaded fast enough | ||
// to possibly be non-blocking (and they have minimal impact anyway). | ||
const MINIMUM_WASTED_MS = 50; | ||
|
||
/** | ||
* Given a simulation's nodeTiming, return an object with the nodes/timing keyed by network URL | ||
* @param {LH.Gatherer.Simulation.Result['nodeTiming']} nodeTimingMap | ||
* @return {Object<string, {node: Node, nodeTiming: LH.Gatherer.Simulation.NodeTiming}>} | ||
*/ | ||
const getNodesAndTimingByUrl = nodeTimingMap => { | ||
const nodes = Array.from(nodeTimingMap.keys()); | ||
return nodes.reduce((map, node) => { | ||
map[node.record && node.record.url] = {node, nodeTiming: nodeTimingMap.get(node)}; | ||
return map; | ||
}, {}); | ||
}; | ||
|
||
class RenderBlockingResources extends Audit { | ||
/** | ||
* @return {!AuditMeta} | ||
*/ | ||
static get meta() { | ||
return { | ||
name: 'render-blocking-resources', | ||
description: 'Eliminate render-blocking resources', | ||
informative: true, | ||
scoreDisplayMode: Audit.SCORING_MODES.NUMERIC, | ||
helpText: | ||
'Resources are blocking the first paint of your page. Consider ' + | ||
'delivering critical JS/CSS inline and deferring all non-critical ' + | ||
'JS/styles. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/blocking-resources).', | ||
requiredArtifacts: ['CSSUsage', 'URL', 'TagsBlockingFirstPaint', 'traces'], | ||
}; | ||
} | ||
|
||
/** | ||
* @param {Artifacts} artifacts | ||
* @param {LH.Audit.Context} context | ||
*/ | ||
static async computeResults(artifacts, context) { | ||
const trace = artifacts.traces[Audit.DEFAULT_PASS]; | ||
const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS]; | ||
const simulatorData = {devtoolsLog, settings: context.settings}; | ||
const traceOfTab = await artifacts.requestTraceOfTab(trace); | ||
const simulator = await artifacts.requestLoadSimulator(simulatorData); | ||
const wastedBytesMap = await RenderBlockingResources.computeWastedCSSBytes(artifacts, context); | ||
|
||
const metricSettings = {throttlingMethod: 'simulate'}; | ||
const metricComputationData = {trace, devtoolsLog, simulator, settings: metricSettings}; | ||
const fcpSimulation = await artifacts.requestFirstContentfulPaint(metricComputationData); | ||
const fcpTsInMs = traceOfTab.timestamps.firstContentfulPaint / 1000; | ||
|
||
const nodesByUrl = getNodesAndTimingByUrl(fcpSimulation.optimisticEstimate.nodeTiming); | ||
|
||
const results = []; | ||
const deferredNodeIds = new Set(); | ||
for (const resource of artifacts.TagsBlockingFirstPaint) { | ||
// Ignore any resources that finished after observed FCP (they're clearly not render-blocking) | ||
if (resource.endTime * 1000 > fcpTsInMs) continue; | ||
// TODO(phulce): beacon these occurences to Sentry to improve FCP graph | ||
if (!nodesByUrl[resource.tag.url]) continue; | ||
|
||
const {node, nodeTiming} = nodesByUrl[resource.tag.url]; | ||
|
||
// Mark this node and all it's dependents as deferrable | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. its |
||
// TODO(phulce): make this slightly more surgical | ||
// i.e. the referenced font asset won't become inlined just because you inline the CSS | ||
node.traverse(node => deferredNodeIds.add(node.id)); | ||
|
||
// "wastedMs" is the download time of the network request, responseReceived - requestSent | ||
const wastedMs = Math.round(nodeTiming.endTime - nodeTiming.startTime); | ||
if (wastedMs < MINIMUM_WASTED_MS) continue; | ||
|
||
results.push({ | ||
url: resource.tag.url, | ||
totalBytes: resource.transferSize, | ||
wastedMs, | ||
}); | ||
} | ||
|
||
if (!results.length) { | ||
return {results, wastedMs: 0}; | ||
} | ||
|
||
const wastedMs = RenderBlockingResources.estimateSavingsWithGraphs( | ||
simulator, | ||
fcpSimulation.optimisticGraph, | ||
deferredNodeIds, | ||
wastedBytesMap | ||
); | ||
|
||
return {results, wastedMs}; | ||
} | ||
|
||
/** | ||
* Estimates how much faster this page would reach FCP if we inlined all the used CSS from the | ||
* render blocking stylesheets and deferred all the scripts. This is more conservative than | ||
* removing all the assets and more aggressive than inlining everything. | ||
* | ||
* *Most* of the time, scripts in the head are there accidentally/due to lack of awareness | ||
* rather than necessity, so we're comfortable with this balance. In the worst case, we're telling | ||
* devs that they should be able to get to a reasonable first paint without JS, which is not a bad | ||
* thing. | ||
* | ||
* @param {Simulator} simulator | ||
* @param {Node} fcpGraph | ||
* @param {Set<string>} deferredIds | ||
* @param {Map<string, number>} wastedBytesMap | ||
* @return {number} | ||
*/ | ||
static estimateSavingsWithGraphs(simulator, fcpGraph, deferredIds, wastedBytesMap) { | ||
const originalEstimate = simulator.simulate(fcpGraph).timeInMs; | ||
|
||
let totalChildNetworkBytes = 0; | ||
const minimalFCPGraph = fcpGraph.cloneWithRelationships(node => { | ||
const canDeferRequest = deferredIds.has(node.id); | ||
const isStylesheet = | ||
node.type === Node.TYPES.NETWORK && | ||
node.record._resourceType === WebInspector.resourceTypes.Stylesheet; | ||
if (canDeferRequest && isStylesheet) { | ||
// We'll inline the used bytes of the stylesheet and assume the rest can be deferred | ||
const wastedBytes = wastedBytesMap.get(node.record.url) || 0; | ||
totalChildNetworkBytes += node.record._transferSize - wastedBytes; | ||
} | ||
|
||
// If a node can be deferred, exclude it from the new FCP graph | ||
return !canDeferRequest; | ||
}); | ||
|
||
// Add the inlined bytes to the HTML response | ||
minimalFCPGraph.record._transferSize += totalChildNetworkBytes; | ||
const estimateAfterInline = simulator.simulate(minimalFCPGraph).timeInMs; | ||
minimalFCPGraph.record._transferSize -= totalChildNetworkBytes; | ||
return Math.round(Math.max(originalEstimate - estimateAfterInline, 0)); | ||
} | ||
|
||
/** | ||
* @param {!Artifacts} artifacts | ||
* @param {LH.Audit.Context} context | ||
* @return {Map<string, number>} | ||
*/ | ||
static async computeWastedCSSBytes(artifacts, context) { | ||
const wastedBytesByUrl = new Map(); | ||
try { | ||
const results = await UnusedCSS.audit(artifacts, context); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe a TODO that this should be pulled out into a computed artifact? |
||
for (const item of results.details.items) { | ||
wastedBytesByUrl.set(item.url, item.wastedBytes); | ||
} | ||
} catch (_) {} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. whats the failure mode here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. assume all bytes were used and inline all CSS |
||
|
||
return wastedBytesByUrl; | ||
} | ||
|
||
/** | ||
* @param {!Artifacts} artifacts | ||
* @param {LH.Audit.Context} context | ||
* @return {AuditResult} | ||
*/ | ||
static async audit(artifacts, context) { | ||
const {results, wastedMs} = await RenderBlockingResources.computeResults(artifacts, context); | ||
|
||
let displayValue = ''; | ||
if (results.length > 1) { | ||
displayValue = `${results.length} resources delayed first paint by ${wastedMs}ms`; | ||
} else if (results.length === 1) { | ||
displayValue = `${results.length} resource delayed first paint by ${wastedMs}ms`; | ||
} | ||
|
||
const headings = [ | ||
{key: 'url', itemType: 'url', text: 'URL'}, | ||
{ | ||
key: 'totalBytes', | ||
itemType: 'bytes', | ||
displayUnit: 'kb', | ||
granularity: 0.01, | ||
text: 'Size (KB)', | ||
}, | ||
{key: 'wastedMs', itemType: 'ms', text: 'Download Time (ms)', granularity: 1}, | ||
]; | ||
|
||
const summary = {wastedMs}; | ||
const details = Audit.makeTableDetails(headings, results, summary); | ||
|
||
return { | ||
displayValue, | ||
score: ByteEfficiencyAudit.scoreForWastedMs(wastedMs), | ||
rawValue: wastedMs, | ||
details, | ||
}; | ||
} | ||
} | ||
|
||
module.exports = RenderBlockingResources; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wastedCssBytesMap
? Or it's more likecssBytesByUrl
or something in function