Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save haalogen/760b6b9c96ad5cc9c0e1b809838c98fc to your computer and use it in GitHub Desktop.
Save haalogen/760b6b9c96ad5cc9c0e1b809838c98fc to your computer and use it in GitHub Desktop.
Detect unused CSS selectors. Show possible CSS duplicates. Monitor realtime CSS usage.
/* 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 = styleSheet.cssRules; // styleSheet.rules
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 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