Skip to content

Instantly share code, notes, and snippets.

@ZauberNerd
Last active April 12, 2016 12:22
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ZauberNerd/9476304 to your computer and use it in GitHub Desktop.
Save ZauberNerd/9476304 to your computer and use it in GitHub Desktop.
Generates CSS for all elements currently in the viewport (Above The Fold) (unstable and hacky, needs more refactoring).
(function () {
'use strict';
var CSSCriticalPath = function (w, d) {
var css = {};
var findMatchingRules = function (node, pseudo) {
var rules = w.getMatchedCSSRules(node, pseudo);
var duplicate = false;
var rulesArr = null;
var rule = null;
if (rules) {
// find all non-duplicate rules matching the current node
// and push them in an array, using it's selector string as key.
for (var i = 0, l = rules.length; i < l; i += 1) {
rule = rules[i];
rulesArr = css[rule.selectorText] || [];
duplicate = rulesArr.some(_exists.bind(null, rule.cssText));
if (!duplicate) {
rulesArr.push(rule);
}
css[rule.selectorText] = rulesArr;
}
}
};
var parseTree = function () {
// Get a list of all the elements in the view.
var height = w.innerHeight;
var el = d.documentElement;
var NFShow = NodeFilter.SHOW_ELEMENT;
var walker = d.createTreeWalker(el, NFShow, _accept, true);
do {
var node = walker.currentNode;
var rect = node.getBoundingClientRect();
if(rect.top < height) {
// element is in the first scroll of the screen
// now go and find every rule which matches the current
// element or it's pseudo before/after elements
findMatchingRules(node);
findMatchingRules(node, ':before');
findMatchingRules(node, ':after');
}
} while (walker.nextNode());
};
this.generateCSS = function () {
var ret = '';
for (var k in css) {
ret += _mergeRules(css[k]) + '\n';
}
return ret;
};
this.getSelectors = function () {
var selectors = [];
for (var k in css) { selectors.push(k); }
return selectors;
};
parseTree();
};
function _exists(text, r) { return text === r.cssText; }
function _accept() { return NodeFilter.FILTER_ACCEPT; }
function _xhr(url, callback) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onreadystatechange = function () {
var isSuccess = this.status >= 200 &&
this.status < 300 || this.status === 304;
if (this.readyState === 4) {
if (isSuccess) {
callback(null, this.responseText);
} else {
callback(this, null);
}
}
};
xhr.send();
}
function _mergeRules(rules) {
// this function is called once for every distinct selector string
// it takes the first rule from the array, walks through all other
// rules and sets their properties on the first rule to finally return
// the first rule.
var firstRule = rules[0];
for (var i = 1, l = rules.length; i < l; i += 1) {
for (var j = 0, k = rules[i].style.length; j < k; j += 1) {
var key = rules[i].style[j];
var value = rules[i].style.getPropertyValue(key);
// weird: sometimes the value of 'content' isn't quoted.
var isQuoted = value.indexOf('"') > -1 ||
value.indexOf('\'') > -1;
if (key === 'content' && !isQuoted) {
value = String('"' + value + '"');
}
firstRule.style.setProperty(key, value);
}
}
return firstRule.cssText;
}
function _getStyleSheets() {
// finds all stylesheets on a page and returns them as an array
var links = document.getElementsByTagName('link');
var styleSheets = [];
for (var i = 0, l = links.length; i < l; i += 1) {
if (links[i].rel === 'stylesheet') {
styleSheets.push(links[i]);
}
}
return styleSheets;
}
function removeStyleSheets(links) {
// removes all stylesheets from the document which are provided as an array.
for (var i = 0, l = links.length; i < l; i += 1) {
links[i].parentNode.removeChild(links[i]);
}
}
function _loadStyleSheets(links, callback) {
// asynchronously load stylesheets and maintain their order
var finished = 0;
function _rcv(url, err, responseText) {
for (var i = 0, l = links.length; i < l; i += 1) {
if (links[i].href === url) {
links.splice(i, 1, responseText);
}
}
if (++finished === links.length) { callback(links.join('\n')); }
}
for (var i = 0, l = links.length; i < l; i += 1) {
_xhr(links[i].href, _rcv.bind(null, links[i].href));
}
}
function init(links, callback) {
// can be called with an array of objects with a 'href' property set
// if stylesheets on the page are not available via xhr (cross-domain-foo)
if (typeof links === 'function') {
callback = links;
links = undefined;
}
var styleSheets = links || _getStyleSheets();
_loadStyleSheets(styleSheets, function (text) {
var el = document.createElement('style');
el.innerHTML = text;
document.body.appendChild(el);
var cp = new CSSCriticalPath(window, document);
callback(cp, el);
});
}
init([{ href: '/build/main.css' }], function (cp, styleEl) {
if (typeof window.callPhantom === 'function') {
window.callPhantom(cp.getSelectors());
} else {
// replace the original stylesheets with the generated version
// (to show the differences) and copy the contents of the generated
// stylesheet in the clipboard if ctrl+c is pressed on the document
// contex.
var css = cp.generateCSS();
removeStyleSheets(_getStyleSheets());
styleEl.innerHTML = css;
var out = document.createElement('textarea');
out.innerHTML = css;
out.style.cssText = 'position: absolute; top: 0; left: 0;' +
'width: 0; height: 0;';
document.body.appendChild(out);
document.addEventListener('keydown', function (ev) {
if (ev.ctrlKey) {
out.focus();
out.select();
}
}, false);
}
});
}());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment