Forked from victor-homyakov/detect-unused-css-selectors.js
Created
April 29, 2020 07:57
-
-
Save pointofpresence/22915d3b00deb411501be240c2ad4c0e to your computer and use it in GitHub Desktop.
Detect unused CSS selectors. Show possible CSS duplicates. Monitor realtime CSS usage.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable no-var,no-console */ | |
// detect unused CSS selectors | |
(function() { | |
var parsedRules = parseCssRules(); | |
console.log('Parsed CSS rules:', parsedRules); | |
detectDuplicateSelectors(parsedRules); | |
var selectorsToTrack = getSelectorsToTrack(parsedRules); | |
window.selectorStats = { unused: [], added: [], removed: [] }; | |
console.log('Tracking style usage (inspect window.selectorStats for details)...'); | |
setInterval(function() { | |
var newSelectors = getSelectorsToTrack(parseCssRules()); | |
// Calculation order for removed/added/unused is significant | |
var removed = Object.keys(selectorsToTrack) | |
.filter(selector => newSelectors[selector] === undefined); | |
var added = Object.keys(newSelectors) | |
.filter(selector => { | |
if (selectorsToTrack[selector] === undefined) { | |
selectorsToTrack[selector] = 0; | |
return true; | |
} | |
return false; | |
}); | |
var unused = Object.keys(selectorsToTrack) | |
.filter(selector => { | |
if (document.querySelector(selector)) { | |
selectorsToTrack[selector]++; | |
} | |
return selectorsToTrack[selector] === 0; | |
}); | |
var message = []; | |
if (unused.length !== window.selectorStats.unused.length) { | |
message.push(unused.length + ' unused'); | |
} | |
window.selectorStats.unused = unused; | |
if (added.length > 0) { | |
message.push(added.length + ' added'); | |
window.selectorStats.added = added; | |
} | |
if (removed.length > 0) { | |
message.push(removed.length + ' removed', removed); | |
window.selectorStats.removed = removed; | |
} | |
if (message.length > 0) { | |
console.log('Selectors: ' + message.join(', ')); | |
} | |
}, 1000); | |
function parseCssRules() { | |
var styleSheets = document.styleSheets, | |
parsedRules = { | |
fontFaces: [], | |
keyframes: [], | |
media: [], | |
style: [], | |
support: [], | |
unknown: [] | |
}; | |
for (var i = 0; i < styleSheets.length; i++) { | |
var styleSheet = styleSheets[i]; | |
var rules; | |
try { | |
rules = styleSheet.cssRules; // styleSheet.rules | |
} catch (e) { | |
if (styleSheet.ignored) { | |
continue; | |
} | |
console.log(e.name + ' while accessing style sheet', styleSheet.ownerNode); | |
styleSheet.ignored = true; | |
if (e.name === 'SecurityError') { | |
// Security error when accessing cross-origin style sheet. | |
// Possible workaround if we want to analyze content: fetch styleSheet.href | |
// (will anyways have problems with relative urls and @import). | |
// https://discourse.mozilla.org/t/webextensions-porting-access-to-cross-origin-document-stylesheets-cssrules/18359 | |
// Appended style sheet will be discovered in the next iteration | |
loadStyleSheet(styleSheet.href, styleSheet.ownerNode); | |
} | |
continue; | |
} | |
for (var j = 0; j < rules.length; j++) { | |
var rule = rules[j]; | |
var ruleClass = Object.prototype.toString.call(rule).replace(/\[object (.+)]/, '$1'); | |
switch (ruleClass) { | |
case 'CSSFontFaceRule': | |
parsedRules.fontFaces.push(rule.cssText); | |
break; | |
case 'CSSKeyframesRule': | |
parsedRules.keyframes.push(rule.cssText); | |
break; | |
case 'CSSMediaRule': | |
// if (rule.conditionText) | |
parsedRules.media.push(rule.conditionText); | |
break; | |
case 'CSSStyleRule': | |
// if (rule.selectorText) | |
parsedRules.style.push(rule.selectorText); | |
// rule.cssText | |
break; | |
case 'CSSSupportsRule': | |
parsedRules.support.push(rule.conditionText); | |
break; | |
default: | |
parsedRules.unknown.push(rule); | |
} | |
} | |
} | |
return parsedRules; | |
} | |
function loadStyleSheet(href, node) { | |
// node.parentNode.removeChild(node); | |
fetch(href).then(response => response.text()).then(css => { | |
var style = document.createElement('style'); | |
// style.innerText = css; inserts line breaks as `<br>` | |
style.innerHTML = css; | |
// Insert before the original style sheet. | |
// This way broken relative URLs will be fixed by the original rules. | |
node.parentNode.insertBefore(style, node); | |
}); | |
} | |
function detectDuplicateSelectors(parsedRules) { | |
var seenSelectors = {}, | |
duplicatedSelectors = [], | |
duplicatedSequence = []; | |
parsedRules.style.forEach(function(selector) { | |
if (selector in seenSelectors) { | |
duplicatedSelectors.push(selector); | |
duplicatedSequence.push(selector); | |
} else { | |
seenSelectors[selector] = true; | |
if (duplicatedSequence.length > 5) { | |
console.warn('Duplicated sequence of selectors:', duplicatedSequence); | |
} | |
duplicatedSequence = []; | |
} | |
}); | |
if (duplicatedSelectors.length > 0) { | |
console.log('List of all duplicated selectors:', duplicatedSelectors); | |
} | |
} | |
function getSelectorsToTrack(parsedRules) { | |
return parsedRules.style | |
.filter(function(selector) { | |
return !( | |
selector === 'html' || | |
selector.includes(':hover') || | |
selector.includes('::after') || | |
selector.includes('::before') | |
); | |
}) | |
.reduce(function(selectors, selector) { | |
selectors[selector] = 0; | |
return selectors; | |
}, {}); | |
} | |
}()); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment