Skip to content

Instantly share code, notes, and snippets.

@OwenMelbz
Last active November 28, 2017 15:45
Show Gist options
  • Save OwenMelbz/19f0f50a4a95d6fd9c57f2a526864d37 to your computer and use it in GitHub Desktop.
Save OwenMelbz/19f0f50a4a95d6fd9c57f2a526864d37 to your computer and use it in GitHub Desktop.
Critical CSS Scripts

Introduction

This is a very experimental script setup, feel free to modify, do what you wish, you'll need to take these instructions with a pinch of salt. Its designed to allow you to annotate css/scss/less etc files and then using postcss will generate *.critical.css files which can then be inlined using server side includes.

Installation

  1. Skim through the code, updating all the paths/folder structures etc to your requirements.
  2. Add the extra dependencies to your node setup
  3. run node split-critical.js - or add a script alias in your package.json for npm run criticalcss for example.
  4. This will generate a minified globals.critical.css for you to use via SSIs and any other tagged files

Usage

Simply wrap your critical code around comments e.g

/*! critical:start */
background: red;
/*! critical:end */

We recommend using /*! style comments as most minification systems strip out the comments but preseve these.

If you want to extract a section into a seperate file simply add critical:start:about for example to the end to make a about.critical.css

Notes

  • This was built using Laravel Mix - but should be similar for Elixir and others.
  • This doesn't real with autoprefixing, scss compilation or anything, and should be the last script that should run in your chain.
body {
/*! critical:start */
width: 200px;
/*! critical:end */
/*! critical:start:home */
height: 400px;
/*! critical:end */
background: #ff0;
/*! critical:start:work */
color: #ff0;
/*! critical:end */
}
{
"dependencies": {
"cssbeautify": "^0.3.1",
"glob-fs": "^0.1.7",
"postcss": "^6.0.12",
"postcss-clean": "^1.0.3",
}
}
'use strict';
const fs = require('fs');
const postcss = require('postcss');
const Splitter = function(css) {
this.css = css;
this.outputDir = './public/assets/css/';
this.groups = {
'global': postcss.root()
};
this.activeGroup = 'global';
this.split = function () {
this.css.walkRules(rule => {
rule.walkComments(comment => {
const info = this.parseComment(comment);
if (info.isCritical) {
comment.remove();
} else {
return true;
}
if (!info.start) {
return true;
}
let clone = rule.clone();
clone.removeAll();
let atRule = rule.parent.type == 'atrule' ? rule.parent.clone() : null;
if (atRule) {
atRule.removeAll();
atRule.append(clone);
}
this.activeGroup = info.group;
rule.walkDecls(decl => {
if (
decl.source.start.line >= info.start &&
decl.source.end.line < info.end
) {
let newDecl = decl.clone();
clone.append(newDecl);
decl.remove();
}
});
if (clone.nodes.length === 0) {
return true;
}
if (atRule) {
this.groups[info.group].append(atRule);
} else {
this.groups[info.group].append(clone);
}
});
});
this.css.walk(el => {
if (typeof el.nodes == 'object' && !el.nodes.length) {
el.remove();
}
});
this.css.walk(el => {
if (typeof el.nodes == 'object' && !el.nodes.length) {
el.remove();
}
});
return this.writeToFiles();
};
this.parseComment = function (decl) {
let info = {
group: this.activeGroup,
isCritical: decl.text.indexOf('critical:') > -1,
start: decl.source.start.line,
end: null,
};
if (info.isCritical && decl.text.indexOf('start') > -1) {
info.start = decl.source.start.line;
decl.parent.walkComments(comment => {
if (comment.text.indexOf('end') > -1 && !info.end) {
if (comment.source.start.line > decl.source.start.line) {
return info.end = comment.source.start.line;
}
}
});
}
let parts = decl.text.split(':');
if (parts.length == 3) {
let group = parts[2];
if (!this.groups[group]) {
this.groups[group] = postcss.root();
}
info.group = group;
} else {
info.group = 'global';
}
return info;
};
this.writeToFiles = function () {
for (let key in this.groups) {
let group = this.groups[key];
fs.writeFileSync(`${this.outputDir}${key}.critical.css`, group.toString());
}
return this.groups.global;
};
};
module.exports = postcss.plugin('postcss-critical-split', function critically(options) {
return function (css) {
const splitter = new Splitter(css, options || {});
return splitter.split();
}
});
const fs = require('fs');
const postcss = require('postcss');
const clean = require('postcss-clean');
const cssunminifier = require('cssbeautify');
const critical = require('./postcss-critical-split');
const glob = require('glob-fs')({ gitignore: false });
function runSplitter() {
let css = fs.readFileSync('./public/assets/css/app.css', 'utf8');
glob.readdirSync('./public/assets/css/*.critical.css').forEach(file => {
try {
fs.unlinkSync(`./${file}`);
} catch (err) {}
});
postcss(critical)
.process(cssunminifier(css))
.then(result => {
let files = removeDupes(
glob.readdirSync('./public/assets/css/*.critical.css')
);
if (process.env.NODE_ENV == 'production') {
postcss(clean)
.process(result.toString())
.then(r => {
return fs.writeFileSync('./public/assets/css/app.css', r.toString());
});
} else {
fs.writeFileSync('./public/assets/css/app.css', result.toString());
}
files.forEach(file => {
try {
let css = fs.readFileSync(file, 'utf8');
postcss(clean)
.process(css)
.then(r => {
return fs.writeFileSync(file, r.toString());
});
} catch (err) {}
});
//console.log(files.length + ' Critical CSS File(s) Generated');
return true;
});
}
function removeDupes(arr) {
let s = new Set(arr);
let it = s.values();
return Array.from(it);
}
module.exports = runSplitter;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment