├── .eslintrc ├── .jshintrc ├── LICENSE ├── README.md ├── config.js ├── db ├── screenshots.db └── screenshots.sql ├── index.js ├── lib ├── fetchCSS.js └── superpuny.js ├── package-lock.json ├── package.json ├── startScripts ├── prod.js └── test.js ├── views ├── home.handlebars ├── layouts │ └── main.handlebars └── results.handlebars ├── webcontent ├── favicon.ico └── images │ └── shrug.png └── whatcss.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "es6": true, 8 | "mocha": true 9 | }, 10 | "rules": { 11 | "valid-jsdoc": ["error", { 12 | "requireReturn": true, 13 | "requireReturnType": true, 14 | "requireParamDescription": true, 15 | "requireReturnDescription": true 16 | }], 17 | "require-jsdoc": ["error", { 18 | "require": { 19 | "FunctionDeclaration": true, 20 | "MethodDefinition": true, 21 | "ClassDeclaration": true 22 | } 23 | }] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 6 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WhatCSS 2 | WhatCSS: CSS StyleSheet Pageload Analyser/Optimizer 🤷 3 | 4 | **Demo / Documentation: https://WhatCSS.info** 5 | 6 | # About 7 | WhatCSS.info automatically generates a minified version of the bare minimum CSS a user needs to begin interacting with your site. 8 | 9 | # Install and Run 10 | * ```git clone https://github.com/jonroig/whatcss.git``` 11 | * ```npm install``` 12 | * ```node index.js``` 13 | 14 | # CLI 15 | WhatCSS can optimize CSS usage on webpages from the command line. 16 | 17 | ```node whatcss https://whatcss.info -i stackpath.bootstrapcdn.com,facebook.com -e inline``` 18 | 19 | Output is a bunch of JSON. If you elect to take a screenshot, it'll return in PNG/base64 as part of the JSON package. 20 | 21 | # PM2 22 | There are PM2 start scripts here: 23 | 24 | ```node startScripts/test.js``` (includes watching) 25 | 26 | ```node startScripts/prod.js``` 27 | 28 | # Installing on AWS 29 | It might be helpful to install Chrome from scratch on AWS using this script: 30 | 31 | ```curl https://intoli.com/install-google-chrome.sh | bash``` 32 | 33 | ... then uncomment these lines in the config.js 34 | 35 | ``` 36 | // headless: true, 37 | // executablePath: '/usr/bin/google-chrome-stable', 38 | ``` 39 | 40 | # To do 41 | * Handle different screensizes and combine the CSS usage information into a single critical path 42 | * Deal with unused CSS more efficiently 43 | * Work on scaling issues (maybe use a single browser instance?) 44 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | serverPort: 3000, 3 | screenshotsArchive: { 4 | active: true, 5 | numberToArchive: 6, 6 | width: 1280, 7 | height: 1024, 8 | deviceScaleFactor: 2, 9 | }, 10 | minification: { 11 | level: 2, 12 | }, 13 | puppeteer: { 14 | launchConfig: { 15 | // headless: true, 16 | // executablePath: '/usr/bin/google-chrome-stable', 17 | }, 18 | waitUntil: 'domcontentloaded', 19 | userAgent: 'WhatCSS.info/bot', 20 | }, 21 | analytics: ` 22 | 23 | `, 24 | 25 | }; 26 | 27 | module.exports = config; 28 | -------------------------------------------------------------------------------- /db/screenshots.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonroig/whatcss/2f2088586c1c8616ce7bb2dcc6a4d5df985e3464/db/screenshots.db -------------------------------------------------------------------------------- /db/screenshots.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE screenshots ( 2 | id integer PRIMARY KEY, 3 | dateTS DATETIME DEFAULT CURRENT_TIMESTAMP, 4 | thePage text NOT NULL, 5 | pngData text NOT NULL, 6 | originalSize int NOT NULL, 7 | newSize int NOT NULL 8 | ); 9 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | WhatCSS 3 | ... a CSS analyser and minification helper. 4 | 5 | * Copyright 2018 Jon Roig. All rights reserved. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | const express = require('express'); 20 | const compression = require('compression'); 21 | const morgan = require('morgan'); 22 | const exphbs = require('express-handlebars'); 23 | const favicon = require('serve-favicon'); 24 | const sqlite3 = require('sqlite3').verbose(); 25 | const urlParse = require('url-parse'); 26 | 27 | const config = require('./config'); 28 | const { fetchCSS, closeBrowser } = require('./lib/fetchCSS'); 29 | const superpuny = require('./lib/superpuny'); 30 | 31 | const screenshotDb = new sqlite3.Database('./db/screenshots.db'); 32 | 33 | 34 | // app setup... 35 | const app = express(); 36 | app.engine('handlebars', exphbs({ defaultLayout: 'main' })); 37 | app.set('view engine', 'handlebars'); 38 | app.use(compression({ threshold: 0 })); 39 | app.use(morgan('combined')); 40 | app.use(favicon(`${__dirname}/webcontent/favicon.ico`)); 41 | app.use('/images', express.static(`${__dirname}/webcontent/images`)); 42 | 43 | 44 | // home 45 | app.get('/', (req, res) => { 46 | const selectSQL = `SELECT id, dateTS, thePage, originalSize, newSize FROM screenshots ORDER BY dateTS DESC LIMIT ${config.screenshotsArchive.numberToArchive}`; 47 | screenshotDb.all(selectSQL, [], (err, rows) => { 48 | const screenshotArray = rows.map(row => ({ 49 | ...row, 50 | percentage: ((row.newSize / row.originalSize) * 100).toFixed(2), 51 | urlEncodedPage: encodeURIComponent(row.thePage), 52 | })); 53 | 54 | res.render('home', { 55 | config, 56 | screenshotArray, 57 | }); 58 | }) 59 | }); 60 | 61 | 62 | // the analyser 63 | app.get('/getcss', (req, res) => { 64 | let thePageInput = req.query.page || false; 65 | const theFormat = req.query.format || false; 66 | const includeString = req.query.include || false; 67 | const excludeString = req.query.exclude || false; 68 | if (!thePageInput) { 69 | return res.redirect(301, '/getcss?page=whatcss.info'); 70 | } 71 | if (thePageInput.toLowerCase().indexOf('http://') !== 0 && thePageInput.toLowerCase().indexOf('https://') !== 0) { 72 | thePageInput = `http://${thePageInput}`; 73 | } 74 | 75 | // do a little cleanup on the input... 76 | // gotta support emoji domains... duh! 77 | const thePageObj = urlParse(thePageInput); 78 | thePageObj.hostname = superpuny.toAscii(thePageObj.hostname); 79 | let thePage = thePageObj.protocol; 80 | if (thePageObj.slashes) { 81 | thePage += '//'; 82 | } 83 | thePage += `${thePageObj.hostname}${thePageObj.pathname}${thePageObj.query}`; 84 | 85 | const params = { 86 | includeString, 87 | excludeString, 88 | doScreenshot: theFormat !== 'json', 89 | }; 90 | 91 | return fetchCSS.get(thePage, params).then((results) => { 92 | if (results.err) { 93 | if (theFormat === 'json') { 94 | return res.send({ 95 | error: results.err.message, 96 | }); 97 | } 98 | 99 | return res.send(`Error: ${results.err.message}`); 100 | } 101 | 102 | if (theFormat === 'json') { 103 | return res.send(results); 104 | } 105 | 106 | // do a little cleanup on the results to make them more 107 | // user friendly... 108 | Object.keys(results.styleSheets).forEach((styleSheet) => { 109 | results.styleSheets[styleSheet].sourceIsURL = 110 | results.styleSheets[styleSheet].sourceURL && 111 | (results.styleSheets[styleSheet].sourceURL.indexOf('http://') 112 | || results.styleSheets[styleSheet].sourceURL.indexOf('https://')); 113 | }); 114 | 115 | Object.keys(results.filteredStyleSheets).forEach((styleSheet) => { 116 | results.filteredStyleSheets[styleSheet].sourceIsURL = 117 | results.filteredStyleSheets[styleSheet].sourceURL && 118 | (results.filteredStyleSheets[styleSheet].sourceURL.indexOf('http://') 119 | || results.filteredStyleSheets[styleSheet].sourceURL.indexOf('https://')); 120 | }); 121 | 122 | 123 | const output = { 124 | ...results, 125 | percentage: ((results.bootSize / results.originalSize) * 100).toFixed(2), 126 | urlEncodedPage: encodeURIComponent(results.id), 127 | includeString: results.includeArray.join(','), 128 | excludeString: results.excludeArray.join(','), 129 | hasFilteredStylesheets: Object.keys(results.filteredStyleSheets).length > 0, 130 | }; 131 | 132 | return res.render('results', { 133 | thePage, 134 | results: output, 135 | }); 136 | }); 137 | }); 138 | 139 | // access images from screenshots... 140 | // they're stored in a sqlite db 141 | app.get('/img/:id', (req, res) => { 142 | const imgId = req.params.id || false; 143 | if (!imgId) { 144 | return res.send(); 145 | } 146 | 147 | const selectSQL = 'SELECT pngData from screenshots WHERE id = ?'; 148 | return screenshotDb.all(selectSQL, [imgId], (err, rows) => { 149 | if (err || !rows[0] || !rows[0].pngData) { 150 | console.log(err || 'no img data'); 151 | return res.send(); 152 | } 153 | const img = Buffer.from(rows[0].pngData, 'base64'); 154 | return res.type('image/png').send(img); 155 | }); 156 | }); 157 | 158 | 159 | // exit handler 160 | process.on('SIGINT', () => { 161 | closeBrowser(); 162 | }); 163 | 164 | // start the app 165 | app.listen(config.serverPort); 166 | console.log(`WhatCSS started on port ${config.serverPort}`); 167 | -------------------------------------------------------------------------------- /lib/fetchCSS.js: -------------------------------------------------------------------------------- 1 | // based on code from 2 | // https://stackoverflow.com/questions/45106841/chrome-devtools-coverage-how-to-save-or-capture-code-used-code 3 | // thx stereobooster! 4 | const puppeteer = require('puppeteer'); 5 | const urlParse = require('url-parse'); 6 | const CleanCSS = require('clean-css'); 7 | const sqlite3 = require('sqlite3').verbose(); 8 | const config = require('../config'); 9 | 10 | const screenshotDb = new sqlite3.Database('./db/screenshots.db'); 11 | 12 | // single browser thread to control 'em all 13 | let browser = null; 14 | const fetchBrowser = async () => { 15 | if (browser) { 16 | return browser; 17 | } 18 | browser = await puppeteer.launch(config.puppeteer.launchConfig); 19 | const browserVersion = await browser.version(); 20 | console.log('Launched browser', browserVersion); 21 | return browser; 22 | }; 23 | 24 | const closeBrowser = async () => { 25 | if (browser) { 26 | await browser.close(); 27 | browser = null; 28 | console.log('closed browser'); 29 | } 30 | }; 31 | 32 | // the "main function" 33 | // fetches CSS... and take screenshots... 34 | const fetchCSS = { 35 | get: async (thePage, params) => { 36 | 37 | // a little input cleanup 38 | const includeString = params.includeString || false; 39 | const excludeString = params.excludeString || false; 40 | const doScreenshot = params.doScreenshot || false; 41 | 42 | try { 43 | const includeArray = includeString ? includeString.split(',') : []; 44 | const excludeArray = excludeString ? excludeString.split(',') : []; 45 | 46 | console.log(`Fetching ${thePage}\n`); 47 | const theBrowser = await fetchBrowser(); 48 | const page = await theBrowser.newPage({ waitUntil: config.puppeteer.waitUntil }); 49 | 50 | // set the user agent 51 | const userAgent = await theBrowser.userAgent(); 52 | await page.setUserAgent(`${config.puppeteer.userAgent} ${userAgent}`); 53 | await page.evaluate('navigator.userAgent'); 54 | 55 | // set the viewport... 56 | const viewport = { 57 | width: config.screenshotsArchive.width, 58 | height: config.screenshotsArchive.height, 59 | deviceScaleFactor: config.screenshotsArchive.deviceScaleFactor, 60 | }; 61 | await page.setViewport(viewport); 62 | 63 | // Start sending raw DevTools Protocol commands are sent using `client.send()` 64 | // First off enable the necessary "Domains" for the DevTools commands we care about 65 | const client = await page.target().createCDPSession(); 66 | await client.send('Page.enable'); 67 | await client.send('DOM.enable'); 68 | await client.send('CSS.enable'); 69 | const output = { 70 | id: thePage, 71 | includeArray, 72 | excludeArray, 73 | styleSheets: {}, 74 | filteredStyleSheets: {}, 75 | bootCSS: '', 76 | bootSize: 0, 77 | originalSize: 0, 78 | }; 79 | 80 | // handle new styleSheets 81 | client.on('CSS.styleSheetAdded', (stylesheet) => { 82 | const { header } = stylesheet; 83 | 84 | // this is the filter 85 | let shouldInclude = includeArray.length === 0; 86 | 87 | if (!excludeArray.includes('inline') || (includeArray.includes('inline') && ( 88 | header.isInline 89 | || header.sourceURL === '' 90 | || header.sourceURL.startsWith('blob:') 91 | ))) { 92 | shouldInclude = true; 93 | } 94 | 95 | if (includeArray.length > 0) { 96 | const urlObj = urlParse(header.sourceURL); 97 | if (includeArray.includes(urlObj.hostname)) { 98 | shouldInclude = true; 99 | } 100 | } 101 | 102 | if (excludeArray.length > 0) { 103 | const urlObj = urlParse(header.sourceURL); 104 | if (excludeArray.includes(urlObj.hostname)) { 105 | shouldInclude = false; 106 | } 107 | } 108 | 109 | if (shouldInclude) { 110 | output.originalSize += header.length; 111 | output.styleSheets[header.styleSheetId] = { 112 | styleSheetId: header.styleSheetId, 113 | sourceURL: header.sourceURL, 114 | origin: header.origin, 115 | disabled: header.disabled, 116 | isInline: header.isInline, 117 | length: header.length, 118 | }; 119 | } else { 120 | output.filteredStyleSheets[header.styleSheetId] = { 121 | styleSheetId: header.styleSheetId, 122 | sourceURL: header.sourceURL, 123 | origin: header.origin, 124 | disabled: header.disabled, 125 | isInline: header.isInline, 126 | length: header.length, 127 | }; 128 | } 129 | }); 130 | 131 | // Start tracking CSS coverage 132 | await client.send('CSS.startRuleUsageTracking'); 133 | 134 | // navigate and wait... 135 | await page.goto(thePage); 136 | const content = await page.content(); 137 | // console.log(content); 138 | 139 | // get the coverage delta 140 | const rules = await client.send('CSS.takeCoverageDelta'); 141 | 142 | // go through the coverage data and gather all the style 143 | // we're actually using... 144 | const usedSlices = []; 145 | for (const rule of rules.coverage) { 146 | const stylesheet = await client.send('CSS.getStyleSheetText', { 147 | styleSheetId: rule.styleSheetId 148 | }); 149 | 150 | if (rule.used) { 151 | usedSlices.push(stylesheet.text.slice(rule.startOffset, rule.endOffset)); 152 | } 153 | } 154 | 155 | // clean up the output... 156 | const cleanCSSOutput = new CleanCSS(config.minification).minify(usedSlices.join('')); 157 | console.log('cleanCSS', cleanCSSOutput.stats, cleanCSSOutput.errors, cleanCSSOutput.warnings); 158 | output.bootCSS = cleanCSSOutput.styles; 159 | output.bootSize = cleanCSSOutput.styles.length; 160 | 161 | // screenshot setup... 162 | const fullPage = false; 163 | const opts = { 164 | fullPage, 165 | // omitBackground: true 166 | clip: { 167 | x: 0, 168 | y: 0, 169 | width: viewport.width, 170 | height: viewport.height, 171 | }, 172 | }; 173 | 174 | // take the screenshot 175 | if (doScreenshot) { 176 | const buffer = await page.screenshot(opts); 177 | console.log('Screenshot', thePage, viewport); 178 | output.screenshotPng = buffer.toString('base64'); 179 | 180 | if (config.screenshotsArchive.active) { 181 | // screenshots are stored in sqlite 182 | // clear out the old ones first... 183 | const deleteSQL = ` 184 | DELETE FROM screenshots WHERE id > 185 | ( SELECT max(id) FROM 186 | ( SELECT id FROM screenshots ORDER BY dateTS DESC LIMIT ${config.screenshotsArchive.numberToArchive} ) 187 | AS screenshots) 188 | `; 189 | screenshotDb.run(deleteSQL); 190 | 191 | // insert the new screenshot 192 | const insertSQL = ` 193 | INSERT INTO screenshots 194 | (thePage, pngData, originalSize, newSize) 195 | VALUES 196 | (?, ?, ?, ?)`; 197 | screenshotDb.run(insertSQL, [ 198 | thePage, 199 | buffer.toString('base64'), 200 | output.originalSize, 201 | output.bootSize, 202 | ]); 203 | } 204 | } 205 | 206 | // close everything up 207 | await page.close(); 208 | 209 | return output; 210 | } catch (e) { 211 | // just spit the error back to the browser... (for now) 212 | console.log('e', e); 213 | return { 214 | err: e, 215 | }; 216 | } 217 | }, 218 | }; 219 | 220 | module.exports = { 221 | fetchCSS, 222 | closeBrowser, 223 | }; 224 | -------------------------------------------------------------------------------- /lib/superpuny.js: -------------------------------------------------------------------------------- 1 | const uts46 = require('idna-uts46'); 2 | const punycode = require('punycode'); 3 | 4 | const superpuny = { 5 | toAscii: (unicodeString) => { 6 | let punycodeDomainLookup = false; 7 | try { 8 | punycodeDomainLookup = uts46.toAscii(unicodeString); 9 | return punycodeDomainLookup; 10 | } catch (e) { 11 | punycodeDomainLookup = punycode.toASCII(unicodeString); 12 | } 13 | return punycodeDomainLookup; 14 | }, 15 | 16 | toUnicode: (asciiString) => { 17 | let unicodeDomainLookup = 'fail'; 18 | 19 | try { 20 | unicodeDomainLookup = uts46.toUnicode(asciiString); 21 | } catch (e) { 22 | try { 23 | unicodeDomainLookup = punycode.toUnicode(asciiString); 24 | } catch (e) { 25 | // nothing 26 | } 27 | } 28 | 29 | // recentLookupArray.splice(0,0,unicodeDomainLookup); 30 | // recentLookupArray = recentLookupArray.slice(0,100); 31 | 32 | return unicodeDomainLookup; 33 | }, 34 | }; 35 | 36 | module.exports = superpuny; 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whatcss", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "APACHE", 11 | "dependencies": { 12 | "base-64": "^0.1.0", 13 | "clean-css": "^4.2.1", 14 | "commander": "^2.19.0", 15 | "compression": "^1.7.3", 16 | "express": "^4.16.4", 17 | "express-handlebars": "^3.0.0", 18 | "idna-uts46": "^1.1.0", 19 | "morgan": "^1.9.1", 20 | "pm2": "^3.2.4", 21 | "punycode": "^2.1.1", 22 | "puppeteer": "^1.11.0", 23 | "serve-favicon": "^2.5.0", 24 | "sqlite3": "^4.0.4", 25 | "url-parse": "^1.4.4" 26 | }, 27 | "devDependencies": { 28 | "babel-eslint": "^10.0.1", 29 | "eslint": "^5.10.0", 30 | "eslint-config-airbnb": "^17.1.0", 31 | "eslint-plugin-import": "^2.14.0", 32 | "eslint-plugin-jsx-a11y": "^6.1.2", 33 | "eslint-plugin-react": "^7.11.1" 34 | }, 35 | "engines": { 36 | "node": ">10.1.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /startScripts/prod.js: -------------------------------------------------------------------------------- 1 | // simple prod start script 2 | // uses pm2, ignores changes to /db 3 | const pm2 = require('pm2'); 4 | 5 | pm2.connect(() => { 6 | pm2.start([ 7 | { 8 | script: 'index.js', 9 | name: 'WhatCSS', 10 | exec_mode: 'fork', 11 | instances: 1, 12 | watch: false, 13 | env: { 14 | NODE_ENV: 'production', 15 | }, 16 | }, 17 | ], (err) => { 18 | if (err) throw new Error(err); 19 | pm2.disconnect(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /startScripts/test.js: -------------------------------------------------------------------------------- 1 | // simple test start script 2 | // uses pm2, ignores changes to /db 3 | const pm2 = require('pm2'); 4 | 5 | pm2.connect(() => { 6 | pm2.start([ 7 | { 8 | script: 'index.js', 9 | name: 'WhatCSS', 10 | exec_mode: 'fork', 11 | instances: 1, 12 | watch: true, 13 | env: { 14 | NODE_ENV: 'development', 15 | }, 16 | ignore_watch: ['./db'], 17 | }, 18 | ], (err) => { 19 | if (err) throw new Error(err); 20 | pm2.disconnect(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /views/home.handlebars: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |

WhatCSS is Your Page Using?

6 |
7 |
8 |
9 |
10 | 17 | 18 |
19 |
20 | 21 |
22 |
23 |
24 |
25 |
26 | 27 | {{#if config.screenshotsArchive.active}} 28 |
29 |
30 |

Recent

31 |
32 |
33 |
34 |
35 | {{#each screenshotArray}} 36 |
37 |
38 | 39 |
40 |
41 | {{thePage}} 42 |
43 |

44 | Original CSS Size: {{originalSize}}
45 | BootCSS: {{newSize}} ({{percentage}}%) 46 |

47 |
48 |
49 |
50 | {{/each}} 51 |
52 |
53 |
54 | {{/if}} 55 | 56 |
57 |
58 |
59 |

WhatCSS: Shrink Your Initial CSS Pageload by up to 94%?

60 |
61 |
62 |

63 | Most of the CSS you deliver to the client on each page load is useless. 64 |

65 |

66 | WhatCSS.info is a free, opensource CSS page load analyser 67 | and optimizer. 68 | Brought to you by your friends at 69 | i❤️.ws emoji domains, 70 | the app allows you to easily see what CSS styles are required to 71 | display your site. 72 |

73 |

74 | WhatCSS.info automatically generates a minified version of the bare minimum 75 | CSS a user needs to begin interacting with your site. 76 | Dramatically shrink WordPress, Shopify, and Bootstrap themes! 77 | Decrease load time! Optimize CSS delivery for SEO! 81 |

82 |

83 | Wasteful delivery of CSS is bringing you down. 84 |

85 |

86 | Why make users wait? 87 |

88 |
89 |
90 | 91 |
92 |
93 |
94 |

How Does WhatCSS Work?

95 |
96 |
97 |

98 | WhatCSS is essentially a wrapper of an instance of Chrome running 99 | on a remote server. 100 |

101 |

102 | Chrome DevTools already captures CSS usage, but the browser lacks the ability to easily 103 | output and make sense of results. WhatCSS uses 104 | Puppeteer, 107 | Google's Headless Chrome Node API, to control a remote browser instance, navigate to 108 | a site, and return which stylesheets are in use when the page is fully loaded. 109 | ("domcontentloaded" for the nerds.) 110 |

111 |

112 | WhatCSS.info takes those results from Chrome, applies extra minification 113 | with 114 | clean-css, 117 | and returns BootCSS: the essential style information a visitor needs 118 | to view your site. 119 |

120 |
121 |
122 | 123 |
124 |
125 |
126 |

WhatCSS Usage

127 |
128 |
129 |

130 | That's up to you. 131 |

132 |

133 | Armed with the minified BootCSS -- often a tiny fraction of the size of 134 | your site's total CSS load, especially if you're using a WordPress or 135 | Bootstrap theme -- it's trivial to just load the styles you need, defer the 136 | rest of the CSS until after the page renders. 137 |

138 |

139 | In some cases -- a simple site like 140 | WhatCSS.info 141 | -- the BootCSS is all the site needs to be fully functional. With only 5% of the 142 | original CSS from the standard Bootstrap 4 distribution needed to use the site, 143 | we just put the CSS inline. 144 |

145 |

146 | Good enough for a technology demo, maybe doesn't quite meet your actual needs. 147 |

148 |

149 | There's an example of how to do this in the next section, but an easy 150 | variation of this might be to inline the BootCSS and defer the 151 | load of the rest of the CSS. This way, the page becomes immediately 152 | interactive, but the styles needed for additional exploration 153 | come along shortly thereafter. 154 |

155 |

156 | This approach also alleviates the need to make any hard choices when it comes 157 | to cutting CSS, especially if you're just loading some random theme off a CDN. 158 |

159 |

160 | With WhatCSS having an API/CLI and being opensource and whatnot, 161 | it can be integrated to a build process, 162 | automating the BootCSS generation. 163 |

164 |

165 | More sophisticated users might want to divide their CSS into two parts, 166 | the first, the BootCSS, essential for displaying the site, and then the 167 | rest of the stuff. With both files on a CDN, the second can be safely 168 | deferred... or even sub divided into tiny, chunked deferred pieces. 169 |

170 |
171 |
172 | 173 |
174 |
175 |
176 |

1-2-3: How We Optimized WhatCSS.info

177 |
178 |
179 |

180 | Like all things SEO, we followed the 181 | Google PageSpeed Insights Guide to Optimizing CSS Delivery. 185 |

186 |

187 | (View the source of this page to see an example.) 188 |

189 |
    190 |
  1. 191 | Determine what stylesheets are being used from the Bootstrap CDN. 192 |
    193 | 194 | https://whatcss.info/getcss?page=https://whatcss.info&include=stackpath.bootstrapcdn.com&exclude=inline 195 | 196 |
  2. 197 |
  3. 198 | Insert the BootCSS into the HEAD of the document. 199 | The resulting CSS was 4.48% of the original size of Bootstrap. 200 |
  4. 201 |
  5. 202 | Defer Bootstrap using the Google CSS Optimization JavaScript example. 206 |
  6. 207 |
208 |

209 | Easy enough, right? Took about 5 mins... made a huge difference. 210 |

211 |
212 |
213 |
214 | 215 |
216 |
217 |

WhatCSS API

218 |
219 |
220 |

221 | Given that WhatCSS.info is itself a wrapper around an API, it's appropriate 222 | that there should be an API to that API. 223 |

224 |

225 | Pretty straightforward: just append "&format=json" to the URL 226 | and it'll... return JSON. 227 |

228 | 229 | https://whatcss.info/getcss?page=https://whatcss.info&format=json 230 | 231 |
232 |
233 |
234 | 235 | 236 |
237 |
238 |
239 |

WhatCSS CLI

240 |
241 |
242 |

243 | You can run WhatCSS from the command line, but you've got 244 | to hop over to 245 | github.com/jonroig/whatcss to get it. 249 |

250 |

251 | You'll have to have Nodejs on your machine and do the standard 252 | npm install. 253 |

254 |

255 | Example usage:
256 | 257 | node whatcss https://whatcss.info -i stackpath.bootstrapcdn.com,facebook.com -e inline 258 | 259 |

260 |

261 | Output is a bunch of JSON. If you elect to take a screenshot, it'll 262 | return in PNG/base64 as part of the JSON package. 263 |

264 |
265 |
266 | 267 | 268 |
269 |
270 |
271 |

WhatCSS Filters

272 |
273 |
274 |

275 | Since WhatCSS grabs all the CSS currently in use by your browser, the API 276 | provides a way to either explicitly include or ignore CSS stylesheets 277 | from different origins. 278 |

279 |

280 | For instance, CSS from Facebook, Twitter, 281 | or Google is almost always rendered by a custom component... and since 282 | that's out of your control, optimizing that CSS might be best left 283 | to the vendor who included that CSS. (Or, for that matter, those styles 284 | might not interfere with your page load at all.) 285 |

286 |

287 | include/exclude: comma separated list of domains or 'inline' 288 |
289 |

290 | 291 | https://whatcss.info/getcss?page=https://whatcss.info&include=stackpath.bootstrapcdn.com,facebook.com&exclude=inline 292 | 293 |
294 |
295 |
296 | 297 |
298 |
299 |

Contact Us

300 |
301 |
302 |

303 | Twitter: @emoji_domains
304 | Github: github.com/jonroig/whatcss 305 |

306 |
307 |
308 |
309 | -------------------------------------------------------------------------------- /views/layouts/main.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{{config.analytics}}} 5 | 🤷‍ WhatCSS.info: CSS StyleSheet Pageload Analyser/Optimizer 🤷 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 68 | 69 | {{{body}}} 70 | 71 |
72 |
73 | @emoji_domains 74 | | github.com/jonroig/whatcss 75 |
76 | Emoji Domains? i❤️.ws 77 |
78 | © 2019 Domain Research Group
79 | Straight outta Scottsdale, AZ! 80 |
81 | 82 | 86 | 95 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /views/results.handlebars: -------------------------------------------------------------------------------- 1 |
2 |
3 |

CSS Analyser

4 |
5 |
6 |

Results for: {{thePage}}

7 |
8 | 9 |
10 | 11 |

12 | 13 |
14 |
15 |

BootCSS

16 |
17 |
18 |

19 | Original Size: {{results.originalSize}}
20 | BootCSS Size: {{results.bootSize}} ({{results.percentage}}%)
21 |

22 |

23 | 24 |

25 |

26 | You can inline the BootCSS and 27 | defer the rest of your stylesheets. 28 |

29 |

30 | See the Google PageSpeed Insights Guide to Optimizing CSS Delivery for 34 | example code or check out our quick tutorial. 37 |

38 |
39 |
40 |
41 | 42 |
43 |
44 |
45 |

Active CSS StyleSheets

46 |
47 |
48 | {{#each results.styleSheets}} 49 |

50 | styleSheetId: {{this.styleSheetId}}
51 | sourceURL: 52 | {{#if sourceIsURL}} 53 | {{this.sourceURL}} 54 | {{else}} 55 | {{this.sourceURL}} 56 | {{/if}} 57 |
58 | origin: {{this.origin}}
59 | disabled: {{this.disabled}}
60 | isInline: {{this.isInline}}
61 | length: {{this.length}} 62 |

63 | {{/each}} 64 |
65 |
66 | 67 | {{#if results.hasFilteredStylesheets}} 68 |
69 |
70 |
71 |

Filtered CSS StyleSheets

72 |
73 |
74 | {{#each results.filteredStyleSheets}} 75 |

76 | styleSheetId: {{this.styleSheetId}}
77 | sourceURL: 78 | {{#if sourceIsURL}} 79 | {{this.sourceURL}} 80 | {{else}} 81 | {{this.sourceURL}} 82 | {{/if}} 83 |
84 | origin: {{this.origin}}
85 | disabled: {{this.disabled}}
86 | isInline: {{this.isInline}}
87 | length: {{this.length}} 88 |

89 | {{/each}} 90 |
91 |
92 | {{/if}} 93 | 94 |
95 | 96 |
97 |
98 |

Settings

99 |
100 |
101 |
102 |
103 | 104 | 111 |
112 |
113 | 114 | 121 |
122 |
123 | 124 | 131 |
132 | 133 |
134 |
135 |
136 |
137 |
138 | -------------------------------------------------------------------------------- /webcontent/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonroig/whatcss/2f2088586c1c8616ce7bb2dcc6a4d5df985e3464/webcontent/favicon.ico -------------------------------------------------------------------------------- /webcontent/images/shrug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonroig/whatcss/2f2088586c1c8616ce7bb2dcc6a4d5df985e3464/webcontent/images/shrug.png -------------------------------------------------------------------------------- /whatcss.js: -------------------------------------------------------------------------------- 1 | // Command Line Implementation of WhatCSS 2 | // v0.1 3 | 4 | const program = require('commander'); 5 | const urlParse = require('url-parse'); 6 | const fetchCSS = require('./lib/fetchCSS'); 7 | const superpuny = require('./lib/superpuny'); 8 | 9 | // set up the options... 10 | program 11 | .version('0.1') 12 | .description('WhatCSS Command Line Utility\n\nExample:\nwhatcss https://example.com -e www.facebook.com,www.google.com') 13 | .option('-i, --include [list of domains]', 'include: domains.com,domain2.com') 14 | .option('-e, --exclude [list of domains]', 'exclude: domains.com,domain2.com') 15 | .option('-s, --screenshot', 'do screenshot: returns base64 encoded PNG') 16 | .parse(process.argv); 17 | 18 | // should show help if there aren't any args... 19 | if (!program.args || !program.args.length) { 20 | program.help(); 21 | } 22 | 23 | // do a little cleanup on the input... 24 | let theWebsite = program.args[0]; 25 | if (theWebsite.toLowerCase().indexOf('http://') !== 0 && theWebsite.toLowerCase().indexOf('https://') !== 0) { 26 | theWebsite = `http://${theWebsite}`; 27 | } 28 | 29 | // gotta support emoji domains... duh! 30 | const thePageObj = urlParse(theWebsite); 31 | thePageObj.hostname = superpuny.toAscii(thePageObj.hostname); 32 | let thePage = thePageObj.protocol; 33 | if (thePageObj.slashes) { 34 | thePage += '//'; 35 | } 36 | thePage += `${thePageObj.hostname}${thePageObj.pathname}${thePageObj.query}`; 37 | 38 | // bundle things up for the css fetcher 39 | const params = { 40 | includeString: program.include || '', 41 | excludeString: program.exclude || '', 42 | doScreenshot: program.screenshot || false, 43 | }; 44 | 45 | // run the css fetcher 46 | return fetchCSS.get(thePage, params).then((results) => { 47 | if (results.err) { 48 | return console.log({ 49 | error: results.err.message, 50 | }); 51 | } 52 | 53 | return console.log(results); 54 | }); 55 | --------------------------------------------------------------------------------