Forked from victor-homyakov/detect-unused-css-selectors.js
Created
May 30, 2020 19:55
-
-
Save designervoid/0958629adee9b52dafc4048f4cb4e6ba to your computer and use it in GitHub Desktop.
Detect unused CSS selectors. Show possible CSS duplicates. Monitor realtime CSS usage.
This file contains hidden or 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