Skip to content
Open
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
13 changes: 12 additions & 1 deletion core/computed/unused-css.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,20 @@ class UnusedCSS {
usedUncompressedBytes += usedRule.endOffset - usedRule.startOffset;
}

// CSS coverage can report overlapping ranges (e.g. a nested rule that lives
// inside its parent rule's range) or the same rule more than once, so the
// summed used bytes can exceed the stylesheet size. Clamp to avoid computing
// a negative amount of unused (wasted) bytes.
// See https://github.com/GoogleChrome/lighthouse/issues/14718.
usedUncompressedBytes = Math.min(usedUncompressedBytes, totalUncompressedBytes);

const compressedSize = estimateCompressedContentSize(
stylesheetInfo.networkRecord, totalUncompressedBytes, 'Stylesheet');
const percentUnused = (totalUncompressedBytes - usedUncompressedBytes) / totalUncompressedBytes;
// Guard against empty stylesheets, whose 0-length content would otherwise
// make percentUnused NaN.
const percentUnused = totalUncompressedBytes > 0 ?
(totalUncompressedBytes - usedUncompressedBytes) / totalUncompressedBytes :
0;
const wastedBytes = Math.round(percentUnused * compressedSize);

return {
Expand Down
24 changes: 24 additions & 0 deletions core/test/computed/unused-css-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,30 @@ describe('UnusedCSS computed artifact', () => {
assert.equal(map({usedRules: [{startOffset: 0, endOffset: 5}]}).wastedPercent, 0);
});

it('does not report negative waste when used ranges overlap or repeat', () => {
// Overlapping ranges (e.g. a nested rule inside its parent's range) or a
// rule reported as used more than once can sum to more than the sheet size.
const overlapping = map({usedRules: [
{startOffset: 0, endOffset: 5},
{startOffset: 2, endOffset: 4},
]});
assert.equal(overlapping.wastedBytes, 0);
assert.equal(overlapping.wastedPercent, 0);

const duplicated = map({usedRules: [
{startOffset: 0, endOffset: 5},
{startOffset: 0, endOffset: 5},
]});
assert.equal(duplicated.wastedBytes, 0);
assert.equal(duplicated.wastedPercent, 0);
});

it('does not report NaN for an empty stylesheet', () => {
const result = map({content: '', usedRules: []});
assert.equal(result.wastedBytes, 0);
assert.equal(result.wastedPercent, 0);
});

it('correctly computes url', () => {
const expectedPreview = 'dummy';
assert.strictEqual(map({header: {sourceURL: '', isInline: false}}).url, expectedPreview);
Expand Down