├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── gulpfile.js ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | 3 | node_modules/ 4 | npm-debug.log 5 | 6 | test/ 7 | .travis.yml 8 | 9 | gulpfile.js 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2015 Eduardo Boucas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSSential 2 | 3 | > [PostCSS](https://github.com/postcss/postcss) plugin to identify annotated blocks of critical CSS and inline them into a page. 4 | 5 | ## Introduction 6 | 7 | Inlining *critical path CSS* in an HTML file is a performance optimization used in websites. [Jonas Ohlsson](https://jonassebastianohlsson.com/criticalpathcssgenerator/) defines it as: 8 | 9 | > The critical path is the path to render a web page - what's needed before that can happen. CSS Stylesheets block rendering. Until the browser has requested, received, downloaded and parsed your stylesheets, the page will remain blank. By reducing the amount of CSS the browser has to go through, and by inlining it on the page (removing the HTTP request), we can get the page to render much, much faster. 10 | 11 | Manually extracting those bits of critical CSS and appending them to a ` 70 | 71 | 72 | 73 |

Hello world

74 | 75 | 76 | 77 | 78 | 79 | ``` 80 | 81 | ### Options 82 | 83 | The plugin receives as argument an options object, supporting the following properties: 84 | 85 | | Option | Default value | Description | 86 | |------------------|-----------------|---------------------------------------------------------------------------| 87 | | `output` | — | Glob of files to inject inline styles in (e.g. `dest/*.+(html|dust)` | 88 | | `cssComment` | `!cssential` | Content of the CSS comment that marks a block as essential | 89 | | `htmlComment` | `cssential` | Content of the HTML comment that serves as placeholder for inline styles | 90 | | `removeOriginal` | `true` | Whether to remove essential styles from the output style sheet | 91 | 92 | ### Example (Gulp) 93 | 94 | ```js 95 | gulp.task('cssential', function () { 96 | var processors = [ 97 | cssential({ 98 | output: 'views/*.+(html|dust)', 99 | cssComment: '!cssential', 100 | htmlComment: 'cssential', 101 | removeOriginal: true 102 | }) 103 | ]; 104 | 105 | return gulp.src('main.css') 106 | .pipe(plugins.postcss(processors)) 107 | .pipe(gulp.dest('dist/')); 108 | }); 109 | ``` 110 | 111 | Takes `main.css`, looks for blocks marked with `/*!cssential*/` and injects the critical CSS in every HTML or Dust file within the `views/` directory that contain the `` placeholder. 112 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | 3 | var files = ['index.js', 'test/*.js', 'gulpfile.js']; 4 | 5 | gulp.task('lint', function () { 6 | var eslint = require('gulp-eslint'); 7 | return gulp.src(files) 8 | .pipe(eslint()) 9 | .pipe(eslint.format()) 10 | .pipe(eslint.failAfterError()); 11 | }); 12 | 13 | gulp.task('test', function () { 14 | var mocha = require('gulp-mocha'); 15 | return gulp.src('test/*.js', { read: false }) 16 | .pipe(mocha()); 17 | }); 18 | 19 | gulp.task('default', ['lint', 'test']); 20 | 21 | gulp.task('watch', function () { 22 | gulp.watch(files, ['lint', 'test']); 23 | }); 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const postcss = require('postcss') 5 | const uglifycss = require('uglifycss') 6 | const gzipSize = require('gzip-size') 7 | const glob = require('glob') 8 | 9 | function processFile(css, opts) { 10 | return new Promise((resolve, reject) => { 11 | const htmlComment = opts.htmlComment || 'cssential' 12 | const cssComment = opts.cssComment || '!cssential' 13 | const removeOriginal = opts.removeOriginal !== false 14 | 15 | let criticalCss = processNode(css, cssComment, htmlComment) 16 | 17 | // Minifying inline CSS 18 | criticalCss = uglifycss.processString(criticalCss) 19 | 20 | const regexPattern = '' 21 | + '(.*?)' 22 | + '' 23 | 24 | const regex = new RegExp(regexPattern, 'g') 25 | 26 | const inlineStyle = '' 27 | + '' 28 | + '' 29 | 30 | const inlineStyleSize = gzipSize.sync(inlineStyle) 31 | 32 | glob(opts.output, {}, (globError, files) => { 33 | if (globError) { 34 | return reject(globError) 35 | } 36 | 37 | let filesRead = 0 38 | 39 | files.forEach(function (file, index, arr) { 40 | fs.readFile(file, 'utf8', function (fileError, data) { 41 | if (fileError) { 42 | return reject(fileError) 43 | } 44 | 45 | const newFile = data.replace(regex, inlineStyle) 46 | 47 | fs.writeFile(file, newFile, function (err) { 48 | if (err) { 49 | return reject(err) 50 | } 51 | 52 | if (++filesRead === files.length) { 53 | return resolve({ 54 | files: files.length, 55 | size: inlineStyleSize 56 | }) 57 | } 58 | }) 59 | }) 60 | }) 61 | }) 62 | }) 63 | } 64 | 65 | function processNode(node, cssComment, removeOriginal) { 66 | let output = '' 67 | let atRules = {} 68 | 69 | node.walkComments(function (node) { 70 | if (node.text !== cssComment) { 71 | return 72 | } 73 | 74 | let parent = node.parent 75 | 76 | // Remove comment 77 | node.remove() 78 | 79 | if (parent.parent.type === 'atrule') { 80 | atRules[parent.parent.name + ' ' + parent.parent.params] = atRules[parent.parent.name + ' ' + parent.parent.params] || [] 81 | atRules[parent.parent.name + ' ' + parent.parent.params].push(parent.toString()) 82 | } else { 83 | output += parent.toString() 84 | } 85 | 86 | if (removeOriginal) { 87 | if (parent.parent.nodes.length === 1) { 88 | parent.parent.remove() 89 | } else { 90 | parent.remove() 91 | } 92 | } 93 | }) 94 | 95 | Object.keys(atRules).forEach(atRule => { 96 | output += '@' + atRule + '{' + atRules[atRule] + '}' 97 | }) 98 | 99 | return output 100 | } 101 | 102 | module.exports = postcss.plugin('cssential', (opts) => { 103 | opts = opts || {} 104 | 105 | return (css, result) => { 106 | return processFile(css, opts).then(output => { 107 | console.log('[CSSential] Inline CSS injected to ' + output.files + ' files (' + output.size + 'b gzipped)'); 108 | }).catch(error => { 109 | console.log('[CSSential] (!) ERROR: ' + error) 110 | }) 111 | } 112 | }) 113 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-cssential", 3 | "version": "1.1.0", 4 | "description": "PostCSS plugin to inline critical CSS", 5 | "keywords": [ 6 | "postcss", 7 | "css", 8 | "postcss-plugin", 9 | "critical" 10 | ], 11 | "author": "Eduardo Boucas ", 12 | "license": "MIT", 13 | "repository": "eduardoboucas/postcss-cssential", 14 | "bugs": { 15 | "url": "https://github.com/eduardoboucas/postcss-cssential/issues" 16 | }, 17 | "homepage": "https://github.com/eduardoboucas/postcss-cssential", 18 | "dependencies": { 19 | "glob": "^5.0.15", 20 | "gzip-size": "^3.0.0", 21 | "postcss": "^5.0.2", 22 | "uglifycss": "0.0.18" 23 | }, 24 | "devDependencies": { 25 | "chai": "^3.2.0", 26 | "gulp": "^3.9.0", 27 | "gulp-eslint": "^1.0.0", 28 | "gulp-mocha": "^2.1.3" 29 | } 30 | } 31 | --------------------------------------------------------------------------------