Created
February 14, 2018 13:35
-
-
Save xstable/1bab6a6e25f5c876751f10815f29400e to your computer and use it in GitHub Desktop.
aboveTheFoldCss (Critical Path CSS Generator)
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
/** | |
* 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