Skip to content
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

Merged
merged 7 commits into from
Apr 25, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions lighthouse-cli/test/smokehouse/dbw-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ module.exports = {
],
onlyAudits: [
'dom-size',
'link-blocking-first-paint',
'script-blocking-first-paint',
'render-blocking-resources',
'errors-in-console',
],
},
Expand Down
28 changes: 3 additions & 25 deletions lighthouse-cli/test/smokehouse/dobetterweb/dbw-expectations.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,22 +67,6 @@ module.exports = [
'geolocation-on-start': {
score: 0,
},
'link-blocking-first-paint': {
score: 0,
rawValue: '<3000',
extendedInfo: {
value: {
results: {
length: 5,
},
},
},
details: {
items: {
length: 5,
},
},
},
'no-document-write': {
score: 0,
extendedInfo: {
Expand Down Expand Up @@ -126,18 +110,12 @@ module.exports = [
'notification-on-start': {
score: 0,
},
'script-blocking-first-paint': {
'render-blocking-resources': {
score: '<1',
extendedInfo: {
value: {
results: {
length: 2,
},
},
},
rawValue: '>100',
details: {
items: {
length: 2,
length: 7,
},
},
},
Expand Down
3 changes: 1 addition & 2 deletions lighthouse-cli/test/smokehouse/offline-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ module.exports = {
'without-javascript',
'user-timings',
'critical-request-chains',
'link-blocking-first-paint',
'script-blocking-first-paint',
'render-blocking-resources',
'webapp-install-banner',
'splash-screen',
'themed-omnibox',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ module.exports = [
'geolocation-on-start': {
score: 1,
},
'link-blocking-first-paint': {
'render-blocking-resources': {
score: 1,
},
'no-document-write': {
Expand All @@ -42,9 +42,6 @@ module.exports = [
'no-websql': {
score: 1,
},
'script-blocking-first-paint': {
score: 1,
},
'uses-passive-event-listeners': {
score: 1,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,7 @@ class UnusedBytes extends Audit {
const settings = context && context.settings || {};
const simulatorOptions = {
devtoolsLog,
throttlingMethod: settings.throttlingMethod,
throttling: settings.throttling,
settings,
};

return artifacts
Expand Down
213 changes: 213 additions & 0 deletions lighthouse-core/audits/byte-efficiency/render-blocking-resources.js
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);
Copy link
Member

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 like cssBytesByUrl or something in function


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
Copy link
Member

Choose a reason for hiding this comment

The 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);
Copy link
Member

Choose a reason for hiding this comment

The 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 (_) {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whats the failure mode here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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;
Loading