Skip to content

Instantly share code, notes, and snippets.

@xstable
Created February 14, 2018 13:35
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save xstable/1bab6a6e25f5c876751f10815f29400e to your computer and use it in GitHub Desktop.
aboveTheFoldCss (Critical Path CSS Generator)
/**
* Simple Critical CSS and Full CSS extraction widget
*
* @usage
* Copy & paste this widget in the browser console and use the following methods with an optional callback.
*
* window.extractCriticalCSS( callback );
* window.extractFullCSS( callback );
*
* When no callback is provided, the extracted CSS is offered as a file download + printed to the browser console.
*
*/
(function(window) {
/**
* Critical CSS extraction
*
* Based on a concept by PaulKinlan
* @link https://gist.github.com/PaulKinlan/6284142
*
* Made cross browser using
* @link https://github.com/ovaldi/getMatchedCSSRules
*/
var CSSCriticalPath = function(w, d, opts) {
var opt = opts || {};
var css = {};
var inlineCount = 0;
var pushCSS = function(r) {
var stylesheetFile = r.parentStyleSheet.href;
if (!stylesheetFile) {
inlineCount++;
stylesheetFile = 'inline';
} else {
stylesheetFile = stylesheetFile;
}
if (!!css[stylesheetFile] === false) {
css[stylesheetFile] = {
media: r.parentStyleSheet.media,
css: {}
};
}
if (!!css[stylesheetFile].css[r.selectorText] === false) {
css[stylesheetFile].css[r.selectorText] = {};
}
var styles = r.style.cssText.split(/;(?![A-Za-z0-9])/);
for (var i = 0; i < styles.length; i++) {
if (!!styles[i] === false) continue;
var pair = styles[i].split(": ");
pair[0] = pair[0].trim();
pair[1] = pair[1].trim();
css[stylesheetFile].css[r.selectorText][pair[0]] = pair[1];
}
};
var parseTree = function() {
// Get a list of all the elements in the view.
var height = w.innerHeight;
var walker = d.createTreeWalker(d, NodeFilter.SHOW_ELEMENT, function(node) {
return NodeFilter.FILTER_ACCEPT;
}, true);
while (walker.nextNode()) {
var node = walker.currentNode;
var rect = node.getBoundingClientRect();
if (rect.top < height || opt.scanFullPage) {
var rules = w.getMatchedCSSRules(node);
if (!!rules) {
for (var r = 0; r < rules.length; r++) {
pushCSS(rules[r]);
}
}
}
}
};
this.generateCSS = function() {
var finalCSS = "";
var printConsole = (console && console.groupCollapsed);
var consoleCSS;
var cssRule;
var title;
var fileReferences = [];
if (console.clear) {
console.clear();
}
if (printConsole) {
console.log("%cSimple Critical CSS Extraction", "font-size:24px;font-weight:bold");
console.log("For professional Critical CSS generators, see https://github.com/addyosmani/critical-path-css-tools");
}
for (var file in css) {
if (printConsole) {
title = (file === 'inline') ? 'Inline' : 'File: ' + file;
console.groupCollapsed(title);
consoleCSS = '';
}
// line number
var lineNo = finalCSS.split(/\r\n|\r|\n/).length;
fileReferences.push([file, lineNo]);
finalCSS += "/**\n * @file " + file;
if (css[file].media && (css[file].media.length > 1 || css[file].media[0] !== 'all')) {
var media = [];
for (var i = 0; i < css[file].media.length; i++) {
if (!css[file].media[i]) {
continue;
}
media.push(css[file].media[i]);
}
if (media.length > 0) {
media = media.join(' ');
finalCSS += "\n * @media " + media;
}
}
finalCSS += "\n */\n";
for (k in css[file].css) {
cssRule = k + " { ";
for (var j in css[file].css[k]) {
cssRule += j + ": " + css[file].css[k][j] + "; ";
}
cssRule += "}" + "\n";
finalCSS += cssRule;
if (printConsole) {
consoleCSS += cssRule;
}
}
finalCSS += "\n";
if (printConsole) {
console.log(consoleCSS);
console.groupEnd();
}
}
if (printConsole) {
console.groupCollapsed('All Extracted Critical CSS (' + humanFileSize(finalCSS.length) + ')');
} else {
console.log('%cAll:', "font-weight:bold");
}
console.log(finalCSS);
if (printConsole) {
console.groupEnd();
}
return [finalCSS, fileReferences];
};
parseTree();
};
/**
* Full CSS extraction
*
* Based on CSSSteal (chrome plugin)
* @link https://github.com/krasimir/css-steal
*/
var CSSSteal = function() {
var elements = [document.body],
html = null,
styles = [],
indent = ' ';
var getHTMLAsString = function() {
return elements.outerHTML;
};
var toArray = function(obj, ignoreFalsy) {
var arr = [],
i;
for (i = 0; i < obj.length; i++) {
if (!ignoreFalsy || obj[i]) {
arr[i] = obj[i];
}
}
return arr;
}
var getRules = function(a) {
var sheets = document.styleSheets,
result = [],
selectorText;
a.matches = a.matches || a.webkitMatchesSelector || a.mozMatchesSelector || a.msMatchesSelector || a.oMatchesSelector;
for (var i in sheets) {
var rules = sheets[i].rules || sheets[i].cssRules;
for (var r in rules) {
selectorText = rules[r].selectorText ? rules[r].selectorText.split(' ').map(function(piece) {
return piece ? piece.split(/(:|::)/)[0] : false;
}).join(' ') : false;
try {
if (a.matches(selectorText)) {
result.push(rules[r]);
}
} catch (e) {
// can not run matches on this selector
}
}
}
return result;
}
var readStyles = function(els) {
return els.reduce(function(s, el) {
s.push(getRules(el));
s = s.concat(readStyles(toArray(el.children)));
return s;
}, []);
};
var flattenRules = function(s) {
var filterBySelector = function(selector, result) {
return result.filter(function(item) {
return item.selector === selector;
});
}
var getItem = function(selector, result) {
var arr = filterBySelector(selector, result);
return arr.length > 0 ? arr[0] : {
selector: selector,
styles: {}
};
}
var pushItem = function(item, result) {
var arr = filterBySelector(item.selector, result);
if (arr.length === 0) result.push(item);
}
var all = [];
s.forEach(function(rules) {
rules.forEach(function(rule) {
var item = getItem(rule.selectorText, all);
for (var i = 0; i < rule.style.length; i++) {
var property = rule.style[i];
item.styles[property] = rule.style.getPropertyValue(property);
}
pushItem(item, all);
});
});
return all;
};
html = getHTMLAsString();
styles = flattenRules(readStyles(elements));
return styles.reduce(function(text, item) {
text += item.selector + ' {\n';
text += Object.keys(item.styles).reduce(function(lines, prop) {
lines.push(indent + prop + ': ' + item.styles[prop] + ';');
return lines;
}, []).join('\n');
text += '\n}\n';
return text;
}, '');
};
/**
* Cross Browser getMatchedCSSRules
* @link https://github.com/ovaldi/getMatchedCSSRules
*/
(function(factory, global) {
global.getMatchedCSSRules = factory();
})((function(win) {
function matchMedia(mediaRule) {
return window.matchMedia(mediaRule.media.mediaText).matches;
}
function matchesSelector(el, selector) {
var matchesSelector = el.matchesSelector || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector;
if (matchesSelector) {
try {
return matchesSelector.call(el, selector);
} catch (e) {
return false;
}
} else {
var matches = el.ownerDocument.querySelectorAll(selector),
len = matches.length;
while (len && len--) {
if (matches[len] === el) {
return true;
}
}
}
return false;
}
function getMatchedCSSRules(el) {
var matchedRules = [],
sheets = el.ownerDocument.styleSheets,
slen = sheets.length,
rlen, rules, mrules, mrlen, rule, mediaMatched;
if (el.nodeType === 1) {
while (slen && slen--) {
rules = sheets[slen].hasOwnProperty('cssRules') || sheets[slen].hasOwnProperty('rules');
rlen = rules.length;
while (rlen && rlen--) {
rule = rules[rlen];
if (rule instanceof CSSStyleRule && matchesSelector(el, rule.selectorText)) {
matchedRules.push(rule);
} else if (rule instanceof CSSMediaRule) {
if (matchMedia(rule)) {
mrules = rule.cssRules || rule.rules;
mrlen = mrules.length;
while (mrlen && mrlen--) {
rule = mrules[mrlen];
if (rule instanceof CSSStyleRule && matchesSelector(el, rule.selectorText)) {
matchedRules.push(rule);
}
}
}
}
}
}
}
return matchedRules;
}
return function() {
return window.getMatchedCSSRules ? window.getMatchedCSSRules : getMatchedCSSRules;
};
})(window), this);
/* FileSaver.js
* A saveAs() FileSaver implementation.
* 1.3.4
* 2018-01-12 13:14:0
*
* By Eli Grey, http://eligrey.com
* License: MIT
* See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
*/
/*global self */
/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */
/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
var saveAs = saveAs || (function(view) {
"use strict";
// IE <10 is explicitly unsupported
if (typeof view === "undefined" || typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) {
return;
}
var
doc = view.document
// only get URL when necessary in case Blob.js hasn't overridden it yet
,
get_URL = function() {
return view.URL || view.webkitURL || view;
},
save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a"),
can_use_save_link = "download" in save_link,
click = function(node) {
var event = new MouseEvent("click");
node.dispatchEvent(event);
},
is_safari = /constructor/i.test(view.HTMLElement) || view.safari,
is_chrome_ios = /CriOS\/[\d]+/.test(navigator.userAgent),
throw_outside = function(ex) {
(view.setImmediate || view.setTimeout)(function() {
throw ex;
}, 0);
},
force_saveable_type = "application/octet-stream"
// the Blob API is fundamentally broken as there is no "downloadfinished" event to subscribe to
,
arbitrary_revoke_timeout = 1000 * 40 // in ms
,
revoke = function(file) {
var revoker = function() {
if (typeof file === "string") { // file is an object URL
get_URL().revokeObjectURL(file);
} else { // file is a File
file.remove();
}
};
setTimeout(revoker, arbitrary_revoke_timeout);
},
dispatch = function(filesaver, event_types, event) {
event_types = [].concat(event_types);
var i = event_types.length;
while (i--) {
var listener = filesaver["on" + event_types[i]];
if (typeof listener === "function") {
try {
listener.call(filesaver, event || filesaver);
} catch (ex) {
throw_outside(ex);
}
}
}
},
auto_bom = function(blob) {
// prepend BOM for UTF-8 XML and text/* types (including HTML)
// note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF
if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
return new Blob([String.fromCharCode(0xFEFF), blob], {
type: blob.type
});
}
return blob;
},
FileSaver = function(blob, name, no_auto_bom) {
if (!no_auto_bom) {
blob = auto_bom(blob);
}
// First try a.download, then web filesystem, then object URLs
var
filesaver = this,
type = blob.type,
force = type === force_saveable_type,
object_url, dispatch_all = function() {
dispatch(filesaver, "writestart progress write writeend".split(" "));
}
// on any filesys errors revert to saving with object URLs
,
fs_error = function() {
if ((is_chrome_ios || (force && is_safari)) && view.FileReader) {
// Safari doesn't allow downloading of blob urls
var reader = new FileReader();
reader.onloadend = function() {
var url = is_chrome_ios ? reader.result : reader.result.replace(/^data:[^;]*;/, 'data:attachment/file;');
var popup = view.open(url, '_blank');
if (!popup) view.location.href = url;
url = undefined; // release reference before dispatching
filesaver.readyState = filesaver.DONE;
dispatch_all();
};
reader.readAsDataURL(blob);
filesaver.readyState = filesaver.INIT;
return;
}
// don't create more object URLs than needed
if (!object_url) {
object_url = get_URL().createObjectURL(blob);
}
if (force) {
view.location.href = object_url;
} else {
var opened = view.open(object_url, "_blank");
if (!opened) {
// Apple does not allow window.open, see https://developer.apple.com/library/safari/documentation/Tools/Conceptual/SafariExtensionGuide/WorkingwithWindowsandTabs/WorkingwithWindowsandTabs.html
view.location.href = object_url;
}
}
filesaver.readyState = filesaver.DONE;
dispatch_all();
revoke(object_url);
};
filesaver.readyState = filesaver.INIT;
if (can_use_save_link) {
object_url = get_URL().createObjectURL(blob);
setTimeout(function() {
save_link.href = object_url;
save_link.download = name;
click(save_link);
dispatch_all();
revoke(object_url);
filesaver.readyState = filesaver.DONE;
});
return;
}
fs_error();
},
FS_proto = FileSaver.prototype,
saveAs = function(blob, name, no_auto_bom) {
return new FileSaver(blob, name || blob.name || "download", no_auto_bom);
};
// IE 10+ (native saveAs)
if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) {
return function(blob, name, no_auto_bom) {
name = name || blob.name || "download";
if (!no_auto_bom) {
blob = auto_bom(blob);
}
return navigator.msSaveOrOpenBlob(blob, name);
};
}
FS_proto.abort = function() {};
FS_proto.readyState = FS_proto.INIT = 0;
FS_proto.WRITING = 1;
FS_proto.DONE = 2;
FS_proto.error =
FS_proto.onwritestart =
FS_proto.onprogress =
FS_proto.onwrite =
FS_proto.onabort =
FS_proto.onerror =
FS_proto.onwriteend =
null;
return saveAs;
}(
window
));
// public extract Critical CSS method
window.extractCriticalCSS = function(callback) {
var cp = new CSSCriticalPath(window, document);
var result = cp.generateCSS();
var css = result[0];
var files = result[1];
try {
var isFileSaverSupported = (callback) ? true : !!new Blob;
} catch (e) {}
if (!isFileSaverSupported) {
alert('Your browser does not support javascript based file download. The critical CSS is printed in the console.')
} else {
var criticalCSS = "/**\n * Simple Critical CSS\n *\n * @url " + document.location.href + "\n * @title " + document.title + "\n * @viewport " + window.innerWidth + "x" + window.innerHeight + "\n * @size " + humanFileSize(css.length) + "\n *\n * Extracted using the Page Speed Optimization CSS extract widget.\n * @link https://wordpress.org/plugins/above-the-fold-optimization/\n * @source https://github.com/optimalisatie/above-the-fold-optimization/blob/master/admin/js/css-extract-widget.js (.min.js)\n *\n * For professional Critical CSS generators see https://github.com/addyosmani/critical-path-css-tools\n *\n * @sources";
var hlines = criticalCSS.split(/\r\n|\r|\n/).length;
hlines += files.length + 3;
for (var i = 0; i < files.length; i++) {
criticalCSS += "\n * @line " + (files[i][1] + hlines) + "\t @file " + files[i][0];
}
criticalCSS += "\n */\n\n";
criticalCSS += css;
if (callback) {
return callback(criticalCSS);
}
var blob = new Blob([criticalCSS], {
type: "text/css;charset=utf-8"
});
var path = window.location.pathname;
if (path && path !== '/' && path.indexOf('/') !== -1) {
path = '-' + path.replace(/\/$/, '').split('/').pop();
} else {
path = '-front-page';
}
var filename = 'critical-css' + path + '.css';
saveAs(blob, filename);
}
};
// public extract Full CSS method
window.extractFullCSS = function(callback) {
var css = CSSSteal();
try {
var isFileSaverSupported = (callback) ? true : !!new Blob;
} catch (e) {}
if (console.clear) {
console.clear();
}
console.log("%cFull CSS Extraction", "font-size:24px;font-weight:bold");
if (console.groupCollapsed) {
console.groupCollapsed('Extracted Full CSS (' + humanFileSize(css.length) + ')');
}
console.log(css);
if (console.groupCollapsed) {
console.groupEnd();
}
if (!isFileSaverSupported) {
alert('Your browser does not support javascript based file download. The full CSS is printed in the console.')
} else {
var fullcss = "/**\n * Full CSS\n *\n * @url " + document.location.href + "\n * @title " + document.title + "\n * @size " + humanFileSize(css.length) + "\n *\n * Extracted using the Page Speed Optimization CSS extract widget.\n * @link https://wordpress.org/plugins/above-the-fold-optimization/\n * @source https://github.com/optimalisatie/above-the-fold-optimization/blob/master/admin/js/css-extract-widget.js (.min.js)\n */\n\n" +
css;
if (callback) {
return callback(fullcss);
}
var blob = new Blob([fullcss], {
type: "text/css;charset=utf-8"
});
var path = window.location.pathname;
if (path && path !== '/' && path.indexOf('/') !== -1) {
path = '-' + path.replace(/\/$/, '').split('/').pop();
} else {
path = '-front-page';
}
var filename = 'full-css' + path + '.css';
saveAs(blob, filename);
}
};
function humanFileSize(size) {
var i = Math.floor(Math.log(size) / Math.log(1024));
return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB'][i];
};
})(window);
/**
* @usage
* window.extractCriticalCSS( callback );
* window.extractFullCSS( callback );
* When no callback is provided, the extracted CSS is offered as a file download + printed to the browser console.
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment