From 31847f7e8aaa2dad60ef4aecd30645dde73297e8 Mon Sep 17 00:00:00 2001 From: Peter Bengtsson Date: Wed, 3 Jan 2018 05:57:42 -0500 Subject: [PATCH] strip unused keyframes, fixes #14 (#57) * strip unused keyframes, fixes #14 * remove console log * excess star * tsc confusion * leftover comment --- .editorconfig | 9 ++++ README.md | 4 ++ examples/animations/index.css | 97 ++++++++++++++++++++++++++++++++++ examples/animations/index.html | 21 ++++++++ src/run.js | 57 +++++++++++++++++++- 5 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 .editorconfig create mode 100644 examples/animations/index.css create mode 100644 examples/animations/index.html diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..c6c8b362 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/README.md b/README.md index f5a0ec28..13f056c7 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,10 @@ It can optionally use `PhantomJS` to extract the HTML. JavaScript code has changed the DOM. That means you can extract the critical CSS needed to display properly before the JavaScript has kicked in. +* Ability to analyze the remaining CSS selectors to see which keyframe + animations that they use and use this to delete keyframe definitions + that are no longer needed. + ## State of the project This is highly experimental. diff --git a/examples/animations/index.css b/examples/animations/index.css new file mode 100644 index 00000000..249286d6 --- /dev/null +++ b/examples/animations/index.css @@ -0,0 +1,97 @@ +.loading, .loaded { + margin-top: 100px; + text-align: center; +} + +/* Portrait and Landscape */ +@media only screen + and (min-device-width: 768px) + and (max-device-width: 1024px) + and (-webkit-min-device-pixel-ratio: 1) { + .loading, .loaded { + margin-top: 50px; + } +} + +.loading { + animation: App-logo-spin infinite 5s linear; + height: 80px; +} + +@keyframes +App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.loaded { + animation-name: pulse; + animation-duration: 3s; + animation-iteration-count: infinite; + animation-timing-function: ease-out; +} + +@keyframes pulse { + 0% { + background-color: #fff5f5; + color: #000; + } + 100% { + background-color: #8ac3ff; + color: #eee; + } +} + +.stretcher { + height: 250px; + width: 250px; + margin: 0 auto; + background-color: red; + animation-name: stretch; + animation-duration: 1.5s; + animation-timing-function: ease-out; + animation-delay: 0; + animation-direction: alternate; + animation-iteration-count: infinite; + animation-fill-mode: none; + animation-play-state: running; +} + +@keyframes stretch { + 0% { + transform: scale(.3); + background-color: red; + border-radius: 100%; + } + 50% { + background-color: orange; + } + 100% { + transform: scale(1.5); + background-color: yellow; + } +} + + + +@-webkit-keyframes progress-bar-stripes { + 0% { + background-position: 1rem 0 + } + to { + background-position: 0 0 + } +} + +@-o-keyframes progress-bar-stripes { + 0% { + background-position: 1rem 0 + } + to { + background-position: 0 0 + } +} diff --git a/examples/animations/index.html b/examples/animations/index.html new file mode 100644 index 00000000..107e1c67 --- /dev/null +++ b/examples/animations/index.html @@ -0,0 +1,21 @@ + + + + + + + +

+ L O A D I N G +

+ + + diff --git a/src/run.js b/src/run.js index fa818af8..af43182a 100644 --- a/src/run.js +++ b/src/run.js @@ -10,6 +10,58 @@ const cheerio = require('cheerio') const utils = require('./utils') const url = require('url') +/** + * + * @param {Object} ast + * @return Object + */ +const postProcessKeyframes = ast => { + const activeAnimationNames = new Set() + // First walk the AST to know which animations are ever mentioned + // by the remaining selectors. + csstree.walk(ast, node => { + if (node.type === 'Declaration') { + if ( + node.property.search(/\banimation$/i) > -1 || + node.property.search(/\banimation-name$/i) > -1 + ) { + // E.g. `animation: thename infinite 5s linear` + // Or `animation-name: thename` + let firstName = false + node.value.children.each(child => { + if (child.type === 'Identifier' && child.name && !firstName) { + activeAnimationNames.add(child.name.toLowerCase()) + firstName = true + } + }) + } + } + }) + // This is the function we use to filter children out. + const cleanChildren = (children, callback) => { + return children.filter(child => { + // The reason for the '\bkeyframes$' regex here is because you might + // have CSS that looks like this: + // @-webkit-keyframes progress-bar-stripes { + // ... + // } + // Bootstrap v3 has this for example. + if (child.type === 'Atrule' && child.name.search(/\bkeyframes$/i) > -1) { + const keyframeName = child.prelude.children[0].name + return callback(keyframeName) + } + return true + }) + } + // First convert the AST object into a plain object so we can mutate + // the plain array that is 'children'. + const obj = csstree.toPlainObject(ast) + obj.children = cleanChildren(obj.children, keyframename => { + return activeAnimationNames.has(keyframename.toLowerCase()) + }) + return csstree.fromPlainObject(obj) +} + /** * * @param {{ urls: Array, debug: boolean, loadimages: boolean, skippable: function, browser: any, userAgent: string, withoutjavascript: boolean }} options @@ -294,7 +346,10 @@ const minimalcss = async options => { // The csso.minify() function will solve this, *and* whitespace minify // it too. let finalCss = utils.collectImportantComments(allCombinedCss) - finalCss = csso.minify(finalCss).css + let csstreeAst = csstree.parse(csso.minify(finalCss).css) + csstreeAst = postProcessKeyframes(csstreeAst) + finalCss = csstree.translate(csstreeAst) + const returned = { finalCss, stylesheetAstObjects, stylesheetContents } return Promise.resolve(returned) }