diff --git a/config/html-webpack-inline-critical-css-plugin.js b/config/critters-webpack-plugin.js similarity index 84% rename from config/html-webpack-inline-critical-css-plugin.js rename to config/critters-webpack-plugin.js index 39270652..6aa631f1 100644 --- a/config/html-webpack-inline-critical-css-plugin.js +++ b/config/critters-webpack-plugin.js @@ -3,10 +3,11 @@ const path = require('path'); const parse5 = require('parse5'); const nwmatcher = require('nwmatcher'); const css = require('css'); +const prettyBytes = require('pretty-bytes'); const treeUtils = parse5.treeAdapters.htmlparser2; -const PLUGIN_NAME = 'html-webpack-inline-critical-css-plugin'; +const PLUGIN_NAME = 'critters-webpack-plugin'; const PARSE5_OPTS = { treeAdapter: treeUtils @@ -25,6 +26,11 @@ const ElementExtensions = { return this.tagName; } }, + insertBefore: function (child, referenceNode) { + if (!referenceNode) return this.appendChild(child); + treeUtils.insertBefore(this, child, referenceNode); + return child; + }, appendChild: function (child) { treeUtils.appendChild(this, child); return child; @@ -82,7 +88,13 @@ const DocumentExtensions = { addEventListener: Object }; -module.exports = class HtmlWebpackInlineCriticalCssPlugin { +/** Critters: Webpack Plugin Edition! + * @class + * @param {Object} options + * @param {Boolean} [options.external=true] Fetch and inline critical styles from external stylesheets + * @param {Boolean} [options.async=true] If `false`, only already-inline stylesheets will be reduced to critical rules. + */ +module.exports = class CrittersWebpackPlugin { constructor(options) { this.options = options || {}; } @@ -112,6 +124,7 @@ module.exports = class HtmlWebpackInlineCriticalCssPlugin { const externalSheets = document.querySelectorAll('link[rel="stylesheet"]'); Promise.all(externalSheets.map(function(link) { + if (self.options.external===false) return; const href = link.getAttribute('href'); if (href.match(/^(https?:)?\/\//)) return Promise.resolve(); const filename = path.resolve(outputPath, href.replace(/^\//, '')); @@ -121,7 +134,12 @@ module.exports = class HtmlWebpackInlineCriticalCssPlugin { const style = document.createElement('style'); style.$$name = href; style.appendChild(document.createTextNode(sheet)); - link.parentNode.appendChild(style); // @TODO insertBefore + link.parentNode.insertBefore(style, link.nextSibling); + if (self.options.async) { + link.setAttribute('rel', 'preload'); + link.setAttribute('as', 'style'); + link.setAttribute('onload', "this.rel='stylesheet'"); + } }); })) .then(function() { @@ -199,8 +217,9 @@ module.exports = class HtmlWebpackInlineCriticalCssPlugin { } style.appendChild(document.createTextNode(sheet)); } - const name = style.$$name; - console.log('\u001b[32mInlined CSS Size' + (name ? (' ('+name+')') : '') + ': ' + (sheet.length / 1000).toPrecision(2) + 'kb (' + ((before.length - sheet.length) / before.length * 100 | 0) + '% of original ' + (before.length / 1000).toPrecision(2) + 'kb)\u001b[39m'); + const name = style.$$name ? style.$$name.replace(/^\//, '') : 'inline CSS'; + const percent = (before.length - sheet.length) / before.length * 100 | 0; + console.log('\u001b[32mCritters: inlined ' + prettyBytes(sheet.length) + ' (' + percent + '% of original ' + prettyBytes(before.length) + ') of ' + name + '.\u001b[39m'); }); } }; diff --git a/webpack.config.js b/webpack.config.js index 89df2021..d640dbf9 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -9,7 +9,7 @@ const PreloadWebpackPlugin = require('preload-webpack-plugin'); const ReplacePlugin = require('webpack-plugin-replace'); const CopyPlugin = require('copy-webpack-plugin'); const WorkboxPlugin = require('workbox-webpack-plugin'); -const HtmlInlineCssPlugin = require('./config/html-webpack-inline-critical-css-plugin'); +const CrittersPlugin = require('./config/critters-webpack-plugin'); const WatchTimestampsPlugin = require('./config/watch-timestamps-plugin'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; @@ -175,8 +175,11 @@ module.exports = function(_, env) { // Inject for resources isProd && new PreloadWebpackPlugin(), - isProd && new HtmlInlineCssPlugin(), + isProd && new CrittersPlugin({ + // convert critical'd to + async: true + }), // Inline constants during build, so they can be folded by UglifyJS. new webpack.DefinePlugin({