├── .bundle └── config ├── .gitignore ├── .nvmrc ├── .ruby-version ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── _audit ├── 1-network.md ├── 2-timeline.md ├── 3-pagespeed.md ├── 4-webpagetest.md ├── 5-sitecost.md ├── 6-yslow.md ├── 90-activity-devtools.md └── 91-activity-audit-services.md ├── _automation ├── 1-npm.md ├── 2-gulp.md ├── 3-grunt.md └── 4-ghooks.md ├── _budgets ├── 1-grunt-perfbudget.md ├── 2-grunt-phantomas.md ├── 90-activity-testing.md └── 91-activity-ci.md ├── _config.yml ├── _drupal ├── 1-magic.md ├── 2-advagg.md ├── 3-modernizr.md ├── 4-cdn.md ├── 6-lazyloader.md ├── 7-picture.md ├── 8-uglify.md ├── 90-activity-cssjs.md ├── 91-activity-theming.md └── 92-activity-media.md ├── _foundation ├── 1-concatenation.md ├── 2-minification.md ├── 3-compression.md └── 4-sharding.md ├── _img ├── logo.svg └── maps-google.png ├── _includes ├── critical.css ├── goals-codebase.html ├── goals.html ├── list-tasks.html └── list-tools.html ├── _js ├── progress.js ├── sample1.js └── sample2.js ├── _layouts └── page.html ├── _sass ├── bootstrap │ ├── _button.scss │ ├── _container.scss │ ├── _jumbotron.scss │ ├── _navbar.scss │ ├── _page-header.scss │ └── _panel.scss ├── components │ ├── _abbr.scss │ ├── _column.scss │ ├── _main.scss │ ├── _repo.scss │ ├── _tasks.scss │ ├── _tool.scss │ └── _wifi.scss ├── global │ └── _type.scss └── main.scss ├── _workflow ├── 1-gulp-concat.md ├── 2-gulp-minify-css.md ├── 3-gulp-htmlmin.md ├── 4-gulp-uglify.md ├── 5-gulp-imagemin.md ├── 6-critical.md ├── 7-gulp-uncss.md ├── 8-fontfaceobserver.md ├── 90-activity-css.md ├── 91-activity-fonts.md └── 91-activity-js.md ├── about.html ├── audit.html ├── budgets.html ├── config.rb ├── css └── .gitkeep ├── drupal.html ├── examples ├── critical │ └── critical.sh ├── grunt-perfbudget │ ├── Gruntfile.js │ ├── README.md │ └── package.json └── phantomas │ └── phantomas--assert-requests.sh ├── foundation.html ├── gulpfile.js ├── img └── .gitkeep ├── index.html ├── js └── .gitkeep ├── package.json └── workflow.html /.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_PATH: .vendor/bundle 3 | BUNDLE_DISABLE_SHARED_GEMS: '1' 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Bundler 2 | .vendor 3 | 4 | # Jekyll 5 | _site 6 | 7 | # npm 8 | node_modules 9 | 10 | # Sass 11 | .sass-cache 12 | css 13 | 14 | # JS pipeline 15 | js 16 | 17 | # Image pipeline 18 | img 19 | 20 | # API keys 21 | settings.json 22 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v0.12.7 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.0.0-p451 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.12 4 | before_script: 5 | - bundle install 6 | script: 7 | - ./node_modules/.bin/gulp test 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # Pull gems from RubyGems 2 | source 'https://rubygems.org' 3 | 4 | gem 'jekyll', '~>2.5.3' 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | blankslate (2.1.2.4) 5 | celluloid (0.16.0) 6 | timers (~> 4.0.0) 7 | classifier-reborn (2.0.3) 8 | fast-stemmer (~> 1.0) 9 | coffee-script (2.4.1) 10 | coffee-script-source 11 | execjs 12 | coffee-script-source (1.9.1.1) 13 | colorator (0.1) 14 | execjs (2.5.2) 15 | fast-stemmer (1.0.2) 16 | ffi (1.9.8) 17 | hitimes (1.2.2) 18 | jekyll (2.5.3) 19 | classifier-reborn (~> 2.0) 20 | colorator (~> 0.1) 21 | jekyll-coffeescript (~> 1.0) 22 | jekyll-gist (~> 1.0) 23 | jekyll-paginate (~> 1.0) 24 | jekyll-sass-converter (~> 1.0) 25 | jekyll-watch (~> 1.1) 26 | kramdown (~> 1.3) 27 | liquid (~> 2.6.1) 28 | mercenary (~> 0.3.3) 29 | pygments.rb (~> 0.6.0) 30 | redcarpet (~> 3.1) 31 | safe_yaml (~> 1.0) 32 | toml (~> 0.1.0) 33 | jekyll-coffeescript (1.0.1) 34 | coffee-script (~> 2.2) 35 | jekyll-gist (1.2.1) 36 | jekyll-paginate (1.1.0) 37 | jekyll-sass-converter (1.3.0) 38 | sass (~> 3.2) 39 | jekyll-watch (1.2.1) 40 | listen (~> 2.7) 41 | kramdown (1.6.0) 42 | liquid (2.6.2) 43 | listen (2.10.0) 44 | celluloid (~> 0.16.0) 45 | rb-fsevent (>= 0.9.3) 46 | rb-inotify (>= 0.9) 47 | mercenary (0.3.5) 48 | parslet (1.5.0) 49 | blankslate (~> 2.0) 50 | posix-spawn (0.3.11) 51 | pygments.rb (0.6.3) 52 | posix-spawn (~> 0.3.6) 53 | yajl-ruby (~> 1.2.0) 54 | rb-fsevent (0.9.4) 55 | rb-inotify (0.9.5) 56 | ffi (>= 0.5.0) 57 | redcarpet (3.2.3) 58 | safe_yaml (1.0.4) 59 | sass (3.4.13) 60 | timers (4.0.1) 61 | hitimes 62 | toml (0.1.2) 63 | parslet (~> 1.5.0) 64 | yajl-ruby (1.2.1) 65 | 66 | PLATFORMS 67 | ruby 68 | 69 | DEPENDENCIES 70 | jekyll (~> 2.5.3) 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Chris Ruppel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Frontend Performance Training 2 | 3 | This repo contains slides and materials to help you learn about frontend performance. From basic principles to workflow automation, you and your team can learn how to audit sites, fix problems, and stick to a performance budget throughout the life of a project. 4 | 5 | ## Learning Objectives 6 | 7 | - **Create a foundation of knowledge** which allows you to optimize an existing site. 8 | - **Confidently build a performant site** from scratch while balancing other priorities like feature backlogs and deadlines. 9 | - **Use automated workflow tools** to check every change you make to a site, ensuring that major changes in performance do not go unnoticed during the development cycle. 10 | 11 | ## History 12 | 13 | * [DrupalCon NOLA 2016](https://events.drupal.org/neworleans2016/training/frontend-performance) 14 | * [DrupalCon LA 2015](https://events.drupal.org/losangeles2015/training/frontend-performance-training) 15 | * [DrupalCon Barcelona 2015](https://events.drupal.org/barcelona2015/training/frontend-performance-training) 16 | 17 | # Installation 18 | 19 | This repo uses Ruby and npm to power a Jekyll site. Assuming you already have [Homebrew](http://brew.sh/), the following commands will install the whole training kit for you (run them at the root of the repo). 20 | 21 | # NOTE: NEVER USE `sudo` TO INSTALL! 22 | 23 | ## OS X 24 | 25 | ``` 26 | # update homebrew 27 | brew update 28 | 29 | # install node version manager the specific version 30 | # of node.js needed by these training materials 31 | brew install nvm 32 | nvm install v0.12.7 33 | nvm use 34 | 35 | # install rbenv 36 | brew install rbenv 37 | brew install ruby-build 38 | 39 | # install ruby 40 | rbenv install 2.0.0-p451 41 | 42 | # install the rest of the tools 43 | gem install bundler 44 | bundle install 45 | npm install -g gulp 46 | npm install 47 | 48 | # run the development server 49 | gulp bs 50 | ``` 51 | 52 | ## Debian/Ubuntu 53 | 54 | ``` 55 | # update repositories 56 | sudo apt-get update 57 | 58 | # check for old node package 59 | dpkg --get-selections | grep node 60 | 61 | # if old node package installation found by previous command, 62 | # we recommend removing it to avoid name collisions 63 | sudo apt-get remove --purge node 64 | 65 | # install nvm (the Node version manager) 66 | # See https://github.com/creationix/nvm#install-script 67 | curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.0/install.sh | bash 68 | 69 | # install the version of node used in these examples 70 | # if "nvm: command not found" error, close and reopen your terminal or session 71 | nvm install v0.12.7 72 | nvm use 73 | 74 | # install rvm 75 | \curl -sSL https://get.rvm.io | bash -s stable --rails 76 | source ~/.rvm/scripts/rvm 77 | 78 | # install ruby 79 | rvm install ruby-2.0.0-p451 80 | gem install bundler 81 | 82 | # install the rest of the tools 83 | cd path/to/this/repo 84 | bundle install 85 | npm install -g gulp 86 | npm install 87 | 88 | # run the development server 89 | gulp bs 90 | ``` 91 | 92 | ## Windows 93 | 94 | Unfortunately at this time we do not have detailed, reliable installation instructions for local Windows development. We recommend using a linux VM for development, and your preferred Windows IDE to edit files. 95 | -------------------------------------------------------------------------------- /_audit/1-network.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Network 3 | type: devtool 4 | 5 | links: 6 | - 7 | text: Chrome's Network tab 8 | type: official 9 | url: https://developers.google.com/web/tools/profile-performance/network-performance/resource-loading 10 | - 11 | text: reading waterfalls 12 | type: more-info 13 | url: http://www.webperformancetoday.com/2010/07/09/waterfalls-101/ 14 | --- 15 | 16 | Network devtools allow you to analyze and identify problematic HTTP traffic. The waterfall displays the full life of every HTTP request, from the moment your browser initiated it to the moment the last byte arrives. For most situations the Network tab is the first and best tool for debugging slow page loads. 17 | -------------------------------------------------------------------------------- /_audit/2-timeline.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Timeline 3 | type: devtool 4 | 5 | links: 6 | - 7 | text: Chrome's Timeline tab 8 | type: official 9 | url: https://developers.google.com/web/tools/profile-performance/evaluate-performance/timeline-tool 10 | - 11 | text: Rendering Performance 12 | type: more-info 13 | url: https://developers.google.com/web/fundamentals/performance/rendering/ 14 | - 15 | text: debugging with the Timeline 16 | type: video 17 | url: https://www.youtube.com/watch?v=Vp524yo0p44 18 | - 19 | text: debugging scrolling performance 20 | type: blog 21 | url: https://fourword.fourkitchens.com/article/fix-scrolling-performance-css-will-change-property 22 | --- 23 | 24 | Timeline has a slighly generic sounding name, but it is very important when you have rendering issues. The most common rendering issues are animation related (e.g. a menu expands in a jerky fashion instead of smoothly). The timeline describes the steps the browser took to determine the layout of your webpage and then paint it on the screen. 25 | -------------------------------------------------------------------------------- /_audit/3-pagespeed.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Google PageSpeed Insights 3 | type: service 4 | 5 | links: 6 | - 7 | text: PageSpeed Insights 8 | type: official 9 | url: https://developers.google.com/speed/pagespeed/ 10 | - 11 | type: gulp 12 | url: https://www.npmjs.com/package/psi 13 | --- 14 | 15 | Google PageSpeed Insights (PSI) is a great first step for auditing, allowing you to quickly analyze and identify major bottlenecks in a site's performance. Simply enter a URL and PageSpeed returns a 0-100 rating of the URL's performance, with clear instructions for correcting the issues it finds. 16 | -------------------------------------------------------------------------------- /_audit/4-webpagetest.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: WebPageTest.org 3 | type: service 4 | 5 | links: 6 | - 7 | text: WebPageTest.org 8 | type: official 9 | url: http://www.webpagetest.org/about 10 | - 11 | text: Test a URL on WPT 12 | url: http://www.webpagetest.org 13 | --- 14 | 15 | WebPageTest (WPT) is currently the most reliable method for determining the perceived performance of your website or app. It combines a large number of factors to produce a **Speed Index** which closely approximates when a user starts to see your webpage. 16 | 17 | You can run the tests from many geographical locations, all data is easily accessible, and it can be saved. 18 | -------------------------------------------------------------------------------- /_audit/5-sitecost.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: What does my site cost? 3 | type: service 4 | 5 | links: 6 | - 7 | text: Measure a site 8 | url: http://whatdoesmysitecost.com/ 9 | --- 10 | 11 | Based on the WPT API, this service compares your site's weight in KB with mobile data plans around the globe. It uses the price and download limit of a monthly plan in each country to give you a high-level glimpse of how expensive it is for people to view your website. 12 | -------------------------------------------------------------------------------- /_audit/6-yslow.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: YSlow 3 | type: service 4 | 5 | links: 6 | - 7 | text: YSlow 8 | type: official 9 | url: http://yslow.org/ 10 | - 11 | text: the YSlow ruleset matrix 12 | type: more-info 13 | url: http://yslow.org/ruleset-matrix/ 14 | --- 15 | 16 | YSlow is another first step for auditing, identifying major bottlenecks in a site's performance. Install the browser extension (or use the API via Grunt) and YSlow gives an overall grade of A-F performance, with itemized issues explaining the problems and possible solutions. One interesting difference between PSI and YSlow is that it has a published matrix explaining how the grade is determined. 17 | -------------------------------------------------------------------------------- /_audit/90-activity-devtools.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Browser DevTools 3 | type: activity 4 | 5 | tasks: 6 | - 7 | id: network-weight 8 | text: Use the Network tab to see the total page weight. 9 | - 10 | id: network-milestones 11 | text: Use the Network tab to see time until the DOMContentLoaded and onLoad events. 12 | - 13 | id: network-heavy 14 | text: Use the Network tab to find the heaviest resource on a web page. 15 | - 16 | id: network-latency 17 | text: Use the Network tab to find the resource with the most latency. 18 | - 19 | id: network-lifecycle 20 | text: Use the Network tab to look at the life cycle of the most latent request and determine where the latency ocurred. 21 | - 22 | id: audit-combinejs 23 | text: Use the Audits tab to determine if some resources could be compressed to save bandwidth. 24 | - 25 | id: audit-uncss 26 | text: Use the Audits tab to see how much CSS on the page is unused. 27 | - 28 | id: audit-image-dimensions 29 | text: Use the Audits tab to find any images that lack width/height dimensions. 30 | --- 31 | -------------------------------------------------------------------------------- /_audit/91-activity-audit-services.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Auditing Services 3 | type: activity 4 | 5 | tasks: 6 | - 7 | id: services-psi 8 | text: Use Google PageSpeed Insights to analyze a web page. Use the information to make a plan for fixing the issues. 9 | - 10 | id: services-wpt-mobile 11 | text: Use WebPageTest to determine how long it takes a web page to start rendering using a Moto E on 3G mobile connection. 12 | - 13 | id: services-wpt-repeat 14 | text: Use WebPageTest to determine the differences between a "cold" load (first-time visit) versus a repeat visit with browser cache primed. 15 | - 16 | id: services-wpt-film-strip 17 | text: Use WebPageTest to generate a film-strip of a web page render. 18 | - 19 | id: services-wpt-spof 20 | text: Use WebPageTest to simulate a network failure in order to test a site's resilience to network problems. 21 | - 22 | id: services-wpt-cost 23 | text: Use WhatDoesMySiteCost to roughly determine the cost of a web page in various countries. 24 | - 25 | id: services-yslow-worst 26 | text: Use YSlow to identify the biggest performance problems with a site. 27 | - 28 | id: services-yslow-js 29 | text: Use YSlow to identify all JavaScript-related scores and determine how to fix them (if needed). 30 | - 31 | id: services-yslow-ruleset 32 | text: Use YSlow with the ruleset designed for small sites and blogs. Compare the score to YSlow v2. 33 | --- 34 | -------------------------------------------------------------------------------- /_automation/1-npm.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: npm 3 | type: tool 4 | 5 | links: 6 | - 7 | text: npm 8 | type: official 9 | url: https://www.npmjs.com/package/npm 10 | --- 11 | 12 | Included with your installation of Node.js is npm, the package manager for JavaScript. Many powerful frontend tools are hosted as packages on npm. Npm also supports the "scripts" property in package.json, which allows you to hook into and run scripts at various stages of packages lifecycle. 13 | -------------------------------------------------------------------------------- /_automation/2-gulp.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Gulp 3 | type: tool 4 | 5 | links: 6 | - 7 | text: Gulp 8 | type: official 9 | url: https://www.npmjs.com/package/gulp 10 | --- 11 | 12 | A task manager specifically designed around node.js' strengths. You have to code a little bit compared to Grunt, but it is noticeably faster in almost all cases. 13 | -------------------------------------------------------------------------------- /_automation/3-grunt.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Grunt 3 | type: tool 4 | 5 | links: 6 | - 7 | text: Grunt 8 | type: official 9 | url: https://www.npmjs.com/package/grunt-cli 10 | - 11 | text: using Grunt to generate critical CSS 12 | type: blog 13 | url: https://fourword.fourkitchens.com/article/use-grunt-and-advagg-inline-critical-css-drupal-7-theme 14 | --- 15 | 16 | Grunt is Gulp's predecessor. While simpler to understand and configure, Grunt is often much slower due to its internal arcitecture decisions. Sometimes that makes it less useful as a task runner since files might get saved quickly during development. 17 | -------------------------------------------------------------------------------- /_automation/4-ghooks.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: git hooks 3 | type: tool 4 | 5 | links: 6 | - 7 | text: ghooks 8 | type: official 9 | url: https://www.npmjs.com/package/ghooks 10 | --- 11 | 12 | Provides a way to run certain tests or operations during git events. For instance, you can lint your code before committing, or run tests before pushing. 13 | -------------------------------------------------------------------------------- /_budgets/1-grunt-perfbudget.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: grunt-perfbudget 3 | type: tool 4 | 5 | links: 6 | - 7 | text: grunt-perfbudget 8 | type: official 9 | url: https://www.npmjs.com/package/grunt-perfbudget 10 | - 11 | text: using grunt-perfbudget 12 | type: blog 13 | url: http://timkadlec.com/2014/05/performance-budgeting-with-grunt/ 14 | - 15 | text: See how much your site costs 16 | url: http://whatdoesmysitecost.com/ 17 | --- 18 | 19 | Uses the WebPageTest API to measure the _Speed Index_ of your site. Could be combined with git hooks to ensure a performance budget is passing before pushing code. 20 | -------------------------------------------------------------------------------- /_budgets/2-grunt-phantomas.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: grunt-phantomas 3 | type: tool 4 | 5 | links: 6 | - 7 | text: grunt-phantomas 8 | type: official 9 | url: https://www.npmjs.com/package/grunt-phantomas 10 | --- 11 | 12 | A tool that allows you to collect trends of performance data. Unlike WPT _Speed Index_, which combined many factors into a single, digestible number, `grunt-phantomas` exposes the raw data and graphs its progress over time. It has built-in support for tests, allowing you to set a limit on any number of factors and easily spot the failing tests. 13 | -------------------------------------------------------------------------------- /_budgets/90-activity-testing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Testing 3 | type: activity 4 | 5 | tasks: 6 | - 7 | id: budgets-test-phantomas 8 | text: Change this site (or the test!) so that it passes the phantomas test. 9 | - 10 | id: budgets-test-critical 11 | text: Change this site (or the test!) so that it passes the critical-test test. 12 | - 13 | id: budgets-test-psi 14 | text: Change this site (or the test!) so that it passes the psi test. 15 | - 16 | id: budgets-test-grunt-perfbudget 17 | text: Use the grunt-perfbudget test in the examples folder. 18 | --- 19 | -------------------------------------------------------------------------------- /_budgets/91-activity-ci.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: GitHub + Travis 3 | type: activity 4 | 5 | tasks: 6 | - 7 | id: ci-fork 8 | text: Fork the training repo on GitHub and clone it locally. 9 | - 10 | id: ci-push 11 | text: Successfully push your changes to your own fork after passing the local performance tests. 12 | - 13 | id: ci-pr 14 | text: Create a PR to the main repo. 15 | - 16 | id: ci- 17 | text: Make changes until the Travis build passes and you see the beautiful green merge button 18 | --- 19 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | name: Frontend Performance Training 2 | permalink: pretty 3 | debug: true 4 | development: true 5 | 6 | markdown: redcarpet 7 | redcarpet: 8 | extensions: ["smart"] 9 | 10 | highlighter: true 11 | exclude: [CONTRIBUTING.md, README.md, SCHEDULE.md, TODO.md config.rb, package.json, gulpfile.js, Gemfile, Gemfile.lock, node_modules, "vendor", bower.json] 12 | 13 | collections: 14 | foundation: 15 | output: true 16 | audit: 17 | output: true 18 | drupal: 19 | output: true 20 | automation: 21 | output: true 22 | workflow: 23 | output: true 24 | budgets: 25 | output: true 26 | 27 | defaults: 28 | - 29 | scope: 30 | path: "" 31 | values: 32 | layout: "page" 33 | - 34 | scope: 35 | path: "tools" 36 | values: 37 | layout: "tool" 38 | 39 | navigation: 40 | - 41 | text: About 42 | title: Basic info about this training 43 | url: /about/ 44 | accesskey: q 45 | - 46 | text: Foundation 47 | title: Learn about the fundamentals of frontend performance 48 | url: /foundation/ 49 | accesskey: w 50 | - 51 | text: Audit 52 | title: Explore performance-related auditing tools 53 | url: /audit/ 54 | accesskey: e 55 | - 56 | text: Drupal 57 | title: Get the best frontend performance out of Drupal 7 58 | url: /drupal/ 59 | accesskey: r 60 | - 61 | text: Workflow 62 | title: Automate your workflow and deployment 63 | url: /workflow/ 64 | accesskey: t 65 | - 66 | text: Budgets 67 | title: Set and maintain a performance budget 68 | url: /budgets/ 69 | accesskey: y 70 | -------------------------------------------------------------------------------- /_drupal/1-magic.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Magic 3 | type: tool 4 | 5 | links: 6 | - 7 | text: the Magic module 8 | type: official 9 | url: https://www.drupal.org/project/magic 10 | - 11 | text: Magic 12 | type: more-info 13 | url: http://fourword.fourkitchens.com/article/magic-frontend-performance-all-themes 14 | --- 15 | 16 | Magic is a lightweight module that implements many frontend performance goodies. Many popular base themes were individually implementing these settings. Using Magic, you can: 17 | 18 | * Move scripts to the footer 19 | * Utilize the `async` attribute on CSS/JS 20 | * Use additional config options for CSS aggregation. 21 | -------------------------------------------------------------------------------- /_drupal/2-advagg.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Advanced CSS/JS Aggregation 3 | type: tool 4 | 5 | links: 6 | - 7 | text: AdvAgg 8 | type: official 9 | url: https://www.drupal.org/project/advagg 10 | - 11 | text: using AdvAgg to inline critical CSS 12 | type: blog 13 | url: https://fourword.fourkitchens.com/article/use-grunt-and-advagg-inline-critical-css-drupal-7-theme 14 | - 15 | text: using AdvAgg to inspect aggregates 16 | type: blog 17 | url: https://fourword.fourkitchens.com/article/use-advagg-information-tab-debug-or-optimize-css-and-js-aggregates-drupal-7 18 | - 19 | text: using prerender in Chrome/IE 20 | type: blog 21 | url: https://fourword.fourkitchens.com/article/prerender-chrome-instant-page-loads 22 | - 23 | text: Bookmarklet to generate DNS prefect link tags 24 | url: https://paul.kinlan.me/quick-script-for-prefetch/ 25 | --- 26 | 27 | AdvAgg is another more advanced option, when your needs have outgrown Magic's functionality. 28 | 29 | * All of Magic's features, plus... 30 | * Stampede protection protects your Drupal site when aggregates are re-generated. 31 | * Prevent more than 4095 CSS selectors in an aggregate. (old IE bug) 32 | * Pre-compress aggregates. Faster than on-demand compression. 33 | * Configure the number of aggregates created. 34 | * Minification of JS (not available in Drupal 7 core). 35 | * Minify and/or inline CSS and JS. 36 | * Asynchronously load CSS/JS/fonts. 37 | * DNS Prefetch hostnames automatically. 38 | -------------------------------------------------------------------------------- /_drupal/3-modernizr.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Modernizr 3 | type: tool 4 | 5 | links: 6 | - 7 | text: the Modernizr module 8 | type: official 9 | url: https://www.drupal.org/project/modernizr 10 | - 11 | text: using Modernizr.load() 12 | type: blog 13 | url: http://fourword.fourkitchens.com/article/one-less-jpg 14 | --- 15 | 16 | Modernizr provides integration for the Modernizr JS library. Its primary performance-oriented feature is the ability to use `Modernizr.load()` to asynchronously request assets based on the feature detection it performs on each individual browser. 17 | 18 | It also contains a Drupal API that lets you build the smallest-possible Modernizr file, so you serve less unused feature tests. 19 | -------------------------------------------------------------------------------- /_drupal/4-cdn.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Content Delivery Network (CDN) 3 | type: tool 4 | 5 | links: 6 | - 7 | text: the CDN module 8 | type: official 9 | url: https://www.drupal.org/project/CDN 10 | --- 11 | 12 | CDN provides integration with Content Delivery Networks, or CDNs. It means that you can host multiple copies of your assets in different geographical locations. Your visitors then receive copies of your assets from the location nearest to them, decreasing latency and speeding up their page loads. 13 | -------------------------------------------------------------------------------- /_drupal/6-lazyloader.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Image Lazyloader 3 | type: tool 4 | 5 | links: 6 | - 7 | text: the Image Lazyloader module 8 | type: official 9 | url: https://www.drupal.org/project/lazyloader 10 | --- 11 | 12 | Lazyloading means that the browser doesn't download images until they are in or near the viewport window. By deferring the loading of images until they are actually viewed, you can save bandwidth. 13 | -------------------------------------------------------------------------------- /_drupal/7-picture.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Picture 3 | type: tool 4 | 5 | links: 6 | - 7 | text: the Picture module 8 | type: official 9 | url: https://www.drupal.org/project/picture 10 | --- 11 | 12 | The Picture module implements ``, the official HTML element for creating responsive images. This method is better than all other approaches because it is steadily achieving native browser support. 13 | -------------------------------------------------------------------------------- /_drupal/8-uglify.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: UglifyJS 3 | type: tool 4 | 5 | links: 6 | - 7 | text: the UglifyJS module 8 | type: official 9 | url: https://www.drupal.org/project/uglifyjs 10 | - 11 | text: using UglifyJS 12 | type: blog 13 | url: http://fourword.fourkitchens.com/article/minified-javascript-fly 14 | --- 15 | 16 | This module uses an external service to minify JS on the fly. Drupal 7 core does not have the ability to minify JS so this results in smaller JS aggregates than is possible with Drupal core. 17 | 18 | Requires Pressflow 7.20 or later. 19 | -------------------------------------------------------------------------------- /_drupal/90-activity-cssjs.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CSS/JS optimization 3 | type: activity 4 | 5 | tasks: 6 | - 7 | id: drupal-cssjs-magic-bottom 8 | text: Use the Magic module to move your scripts to the bottom. 9 | - 10 | id: drupal-cssjs-magic-jquery 11 | text: Use the Magic module to retain jQuery in the <head>. 12 | - 13 | id: drupal-cssjs-advagg-compressed 14 | text: Use the AdvAgg module to generate compressed assets. 15 | - 16 | id: drupal-cssjs-advagg-minify 17 | text: Use the AdvAgg module to minify JavaScript. 18 | - 19 | id: drupal-cssjs-advagg-cdn 20 | text: Use the AdvAgg module to reference CDN-hosted jQuery instead of the built-in copy from Drupal core. 21 | - 22 | id: drupal-cssjs-advagg-bundler 23 | text: Use the AdvAgg module to control the number of CSS/JS aggregates on the Drupal site. 24 | - 25 | id: drupal-cssjs-advagg-load 26 | text: Use the AdvAgg module to load CSS and JS asynchronously. 27 | --- 28 | -------------------------------------------------------------------------------- /_drupal/91-activity-theming.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Theming optimization 3 | type: activity 4 | 5 | tasks: 6 | - 7 | id: drupal-grunt-criticalcss 8 | text: Use Grunt or Gulp to generate critical CSS for a theme 9 | - 10 | id: drupal-theming-prefetch 11 | text: Add a DNS pre-fetch to your theme's page template 12 | - 13 | id: drupal-theming-prerender 14 | text: Add a prerender to your theme's page template (must use Chrome or IE11 to test). 15 | --- 16 | -------------------------------------------------------------------------------- /_drupal/92-activity-media.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Media optimization 3 | type: activity 4 | 5 | tasks: 6 | - 7 | id: drupal-media-picture 8 | text: Use the Picture module to add responsive images to your site. 9 | - 10 | id: drupal-media-lazyload 11 | text: Use the Image Lazyloader module to defer images until they are actually seen. 12 | --- 13 | -------------------------------------------------------------------------------- /_foundation/1-concatenation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Concatenation 3 | --- 4 | 5 | Concatenation means to combine several assets into one asset. When Drupal combines multiple stylesheets into an aggregate, that is concatenation. Another common concatenation is inlining assets into another asset (e.g. inline an image into CSS, or inlining critical CSS into HTML) 6 | -------------------------------------------------------------------------------- /_foundation/2-minification.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Minification 3 | --- 4 | 5 | Minification is when all unimportant information is removed from a file. This typically means all whitespace and line-breaks are removed, but can also mean that JS variables are renamed to be as short as possible. Generous use of whitespace is required to maintain organized files, but the typical user never looks at your files so the whitespace is unnecessary bulk. 6 | -------------------------------------------------------------------------------- /_foundation/3-compression.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Compression 3 | --- 4 | 5 | Compression is when your server zips up an asset before sending it to the browser. In most cases it is more efficient to zip the asset, send the smaller version, and unzip it in the browser, rather than transferring the uncompressed asset over the internet. 6 | -------------------------------------------------------------------------------- /_foundation/4-sharding.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Domain sharding 3 | --- 4 | 5 | Domain sharding is when you set up multiple subdomains in order to "trick" the browser into opening more HTTP connections to a single website. Typically you'll see a domain that has several subdomains: css.example.com, js.example.com etc. Since a browser will only open a certain number of HTTP connections to a single host, domain sharding allows for more simultaneous connections to your server. 6 | -------------------------------------------------------------------------------- /_img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 22 | 23 | 27 | 29 | 32 | 33 | 34 | 38 | 39 | 40 | 41 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /_img/maps-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fourkitchens/frontend-perf/11d63b7dfcd98955c8048826d91cf162d02ccc9f/_img/maps-google.png -------------------------------------------------------------------------------- /_includes/critical.css: -------------------------------------------------------------------------------- 1 | /* This file intentionally left blank. See gulp task: critical */ 2 | -------------------------------------------------------------------------------- /_includes/goals-codebase.html: -------------------------------------------------------------------------------- 1 |

How many of these goals can you complete? Your progress will be automatically saved. Use this site's codebase as a starting point.

2 | -------------------------------------------------------------------------------- /_includes/goals.html: -------------------------------------------------------------------------------- 1 |

How many of these goals can you complete? Your progress will be automatically saved as you check the boxes.

2 | -------------------------------------------------------------------------------- /_includes/list-tasks.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ activity.title }}

3 |
4 | {{ activity.content }} 5 | 6 |
    7 | {% for task in activity.tasks %} 8 |
  1. {{ task.text }}
  2. 9 | {% endfor %} 10 |
11 |
12 | 13 | -------------------------------------------------------------------------------- /_includes/list-tools.html: -------------------------------------------------------------------------------- 1 |
2 |
{{ tool.title }}
3 | 19 |
20 | -------------------------------------------------------------------------------- /_js/progress.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This script stores each visitor's progress via localStorage. 3 | */ 4 | 'use strict'; 5 | 6 | var trainingPrefix = 'frontend-perf-training'; 7 | var trainingName = trainingPrefix + '_dcla_'; 8 | 9 | // 10 | // Check for progress when page loads. 11 | // 12 | if (localStorage.getItem(trainingName) !== null) { 13 | populateStorage(); 14 | } else { 15 | initStorage(); 16 | } 17 | 18 | // 19 | // First-time visit needs to initialize storage. 20 | // 21 | function initStorage() { 22 | localStorage.setItem(trainingName, trainingName); 23 | } 24 | 25 | // 26 | // Populate the page based on past progress 27 | // 28 | function populateStorage() { 29 | var checkboxes = document.querySelectorAll('input[type="checkbox"]'); 30 | 31 | for (var i = 0; i < checkboxes.length; ++i) { 32 | checkboxes[i].checked = (localStorage.getItem(trainingName + checkboxes[i].id) === 'true') ? true : false; 33 | }; 34 | } 35 | 36 | // 37 | // Set status of a checkbox 38 | // 39 | function setStorage(id) { 40 | var checkbox = document.getElementById(id); 41 | var newValue = (checkbox.checked) ? 'true' : 'false'; 42 | 43 | // checked = true, unchecked = false. 44 | localStorage.setItem(trainingName + id, newValue); 45 | } 46 | 47 | // 48 | // Listen for changes and save each change. The entire set of checkboxes gets 49 | // saved each time. 50 | // 51 | var checkboxes = document.querySelectorAll('input[type="checkbox"]'); 52 | 53 | for (var i = 0; i < checkboxes.length; ++i) { 54 | checkboxes[i].addEventListener('change', function listen() { 55 | setStorage(this.id); 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /_js/sample1.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an example JS file that will have its comments stripped out when it 3 | * is minified. 4 | */ 5 | (function () { 6 | var message1 = 'This message will appear in the minified file,'; 7 | var message2 = 'but the variable name will be changed.'; 8 | 9 | // console.log gets stripped out during minification 10 | console.log(message1, message2); 11 | })(); 12 | -------------------------------------------------------------------------------- /_js/sample2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a second JS file that will be combined with the other one. 3 | */ 4 | (function () { 5 | var message1 = 'This is second JS file.'; 6 | var message2 = 'It has been combined with the first.'; 7 | 8 | // console.log gets stripped out during minification 9 | console.log(message1, message2); 10 | })(); 11 | -------------------------------------------------------------------------------- /_layouts/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ page.title }} | {{ site.name }} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% comment %} 16 | 17 | 18 | {% endcomment %} 19 | 20 | 21 | 53 | 54 |
55 | {{ content }} 56 |
57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /_sass/bootstrap/_button.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // Bootstrap button 3 | // ----------------------------------------------------------------------------- 4 | 5 | .btn { 6 | margin-bottom: .5em; 7 | } 8 | -------------------------------------------------------------------------------- /_sass/bootstrap/_container.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // Bootstrap container 3 | // ----------------------------------------------------------------------------- 4 | 5 | .container { 6 | // The default is too wide. 7 | max-width: 980px; 8 | } 9 | 10 | main.container { 11 | // put padding on main container 12 | padding-top: 80px; 13 | padding-bottom: 120px; 14 | } 15 | -------------------------------------------------------------------------------- /_sass/bootstrap/_jumbotron.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // Bootstrap jumbotron 3 | // ----------------------------------------------------------------------------- 4 | 5 | .jumbotron { 6 | h1 { 7 | font-family: $droid-sans; 8 | text-shadow: 1px 1px 0px rgba(#fff, 1); 9 | } 10 | 11 | @media screen and (min-width: 768px) { 12 | h1.slightly-smaller { 13 | font-size: 53px; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /_sass/bootstrap/_navbar.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // Bootstrap navbar 3 | // ----------------------------------------------------------------------------- 4 | 5 | .navbar-inverse .navbar-nav>li>a { 6 | &.active { 7 | color: #fff; 8 | animation: nav-pulse 3s alternate infinite; 9 | } 10 | } 11 | 12 | @keyframes nav-pulse { 13 | 80% { 14 | transform: scale(1); 15 | text-shadow: 0 0 1px rgba(255, 255, 255, 0.6); 16 | } 17 | 90% { 18 | transform: scale(1.04); 19 | text-shadow: 0 0 5px rgba(255, 255, 255, 0.6); 20 | } 21 | 100% { 22 | transform: scale(1); 23 | text-shadow: 0 0 1px rgba(255, 255, 255, 0.6); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /_sass/bootstrap/_page-header.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // Bootstrap page-header 3 | // ----------------------------------------------------------------------------- 4 | 5 | h1, 6 | h2, 7 | h3, 8 | h4, 9 | h5, 10 | h6, 11 | .page-header { 12 | clear: both; 13 | } 14 | -------------------------------------------------------------------------------- /_sass/bootstrap/_panel.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // Bootstrap panel 3 | // ----------------------------------------------------------------------------- 4 | 5 | .panel { 6 | .panel-heading { 7 | font-weight: 700; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /_sass/components/_abbr.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // Abbreviations 3 | // ----------------------------------------------------------------------------- 4 | 5 | abbr { 6 | border-bottom: 1px dotted black; 7 | cursor: help; 8 | } 9 | -------------------------------------------------------------------------------- /_sass/components/_column.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // Columns 3 | // ----------------------------------------------------------------------------- 4 | 5 | .col2 > * { 6 | float: left; 7 | width: 47.5%; 8 | margin-right: 5%; 9 | 10 | &:nth-of-type(2n) { 11 | float: right; 12 | margin-right: 0; 13 | } 14 | } 15 | 16 | .col3 > * { 17 | float: left; 18 | width: 30%; 19 | margin-right: 5%; 20 | 21 | &:nth-of-type(3n) { 22 | float: right; 23 | margin-right: 0; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /_sass/components/_main.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // Main content area 3 | // ----------------------------------------------------------------------------- 4 | 5 | main { 6 | // Classes to make informational sidebars. 7 | // Often used with .panel class. 8 | .float-left { 9 | width: 35%; 10 | float: left; 11 | margin-right: 5%; 12 | } 13 | .float-right { 14 | width: 35%; 15 | float: right; 16 | margin-left: 5%; 17 | } 18 | .float-half { 19 | width: 47.5%; 20 | } 21 | 22 | // General utilities for content 23 | .clear { 24 | clear: both; 25 | } 26 | .def { 27 | font-size: 125%; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /_sass/components/_repo.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // Repo info 3 | // ----------------------------------------------------------------------------- 4 | 5 | .repo { 6 | padding-top: 1em; 7 | padding-left: 1em; 8 | font-size: 2.2em; 9 | } 10 | -------------------------------------------------------------------------------- /_sass/components/_tasks.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // Activities and Tasks 3 | // ----------------------------------------------------------------------------- 4 | 5 | .tasks { 6 | // Allow for some nudging of elements within spacific tasks. 7 | .desc { 8 | position: relative; 9 | 10 | // For the GH+Travis activity 11 | .btn-merge { 12 | position: absolute; 13 | top: -10px; 14 | right: -155px; 15 | } 16 | } 17 | 18 | // Make the checkbox a little bigger 19 | input[type="checkbox"] { 20 | font-size: 1.25em; 21 | margin-right: .25em; 22 | } 23 | 24 | // Mark tasks as "done" 25 | input[type="checkbox"]:checked + .desc { 26 | text-decoration: line-through; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /_sass/components/_tool.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // Tools 3 | // ----------------------------------------------------------------------------- 4 | 5 | .tools { 6 | margin: 2em 0; 7 | } 8 | -------------------------------------------------------------------------------- /_sass/components/_wifi.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // Wifi info 3 | // ----------------------------------------------------------------------------- 4 | 5 | .wifi { 6 | padding-top: 1em; 7 | padding-left: 6em; 8 | font-size: 3em; 9 | } 10 | -------------------------------------------------------------------------------- /_sass/global/_type.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // Fonts 3 | // ----------------------------------------------------------------------------- 4 | 5 | @font-face { 6 | font-family: 'Droid Sans'; 7 | src: url('//fonts.googleapis.com/css?family=Droid+Sans:400'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | $droid-sans: 'Droid Sans', sans-serif; 13 | -------------------------------------------------------------------------------- /_sass/main.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // Main Sass file 3 | // ----------------------------------------------------------------------------- 4 | 5 | // Global styles 6 | @import 'global/type'; 7 | 8 | // Bootstrap overrides 9 | @import 'bootstrap/container'; 10 | @import 'bootstrap/jumbotron'; 11 | @import 'bootstrap/page-header'; 12 | @import 'bootstrap/navbar'; 13 | @import 'bootstrap/panel'; 14 | @import 'bootstrap/button'; 15 | 16 | // Content components 17 | @import 'components/main'; 18 | @import 'components/column'; 19 | @import 'components/wifi'; 20 | @import 'components/repo'; 21 | @import 'components/tool'; 22 | @import 'components/abbr'; 23 | @import 'components/tasks'; 24 | -------------------------------------------------------------------------------- /_workflow/1-gulp-concat.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: gulp-concat 3 | type: tool 4 | 5 | links: 6 | - 7 | text: gulp-concat 8 | type: official 9 | url: https://www.npmjs.com/package/gulp-concat 10 | --- 11 | 12 | Combines files together. Most often used to combine CSS files, or used in conjunction with uglify to create JS aggregates. 13 | -------------------------------------------------------------------------------- /_workflow/2-gulp-minify-css.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: gulp-minify-css 3 | type: tool 4 | 5 | links: 6 | - 7 | text: gulp-minify-css 8 | type: official 9 | url: https://www.npmjs.com/package/gulp-minify-css 10 | --- 11 | 12 | Minifies CSS, removing comments and unnecessary whitespace. 13 | -------------------------------------------------------------------------------- /_workflow/3-gulp-htmlmin.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: gulp-htmlmin 3 | type: tool 4 | 5 | links: 6 | - 7 | text: gulp-htmlmin 8 | type: official 9 | url: https://www.npmjs.com/package/gulp-htmlmin 10 | --- 11 | 12 | Minifies your HTML, removing comments and unnecessary whitespace. 13 | -------------------------------------------------------------------------------- /_workflow/4-gulp-uglify.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: gulp-uglify 3 | type: tool 4 | 5 | links: 6 | - 7 | text: gulp-uglify 8 | type: official 9 | url: https://www.npmjs.com/package/gulp-uglify 10 | --- 11 | 12 | Minifies your JavaScript, removing whitespace and long variable names. 13 | -------------------------------------------------------------------------------- /_workflow/5-gulp-imagemin.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: gulp-imagemin 3 | type: tool 4 | 5 | links: 6 | - 7 | text: gulp-imagemin 8 | type: official 9 | url: https://www.npmjs.com/package/gulp-imagemin 10 | --- 11 | 12 | Compresses binary images and minifies SVG files, making your image assets as small as possible. 13 | -------------------------------------------------------------------------------- /_workflow/6-critical.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: critical 3 | type: tool 4 | 5 | links: 6 | - 7 | text: critical 8 | type: official 9 | url: https://www.npmjs.com/package/critical 10 | - 11 | text: the critical rendering path 12 | type: more-info 13 | url: https://developers.google.com/web/fundamentals/performance/critical-rendering-path/?hl=en 14 | - 15 | text: generating critical CSS with gulp 16 | type: blog 17 | url: https://fourword.fourkitchens.com/article/use-gulp-automate-your-critical-path-css 18 | --- 19 | 20 | Helps identify and isolate CSS that is critical to your initial page render. Since only a portion of your site is displayed initially, identifying these bits of critical CSS and serving them inline along with your HTML can help mobile phones in particular render the page faster. 21 | -------------------------------------------------------------------------------- /_workflow/7-gulp-uncss.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: gulp-uncss 3 | type: tool 4 | 5 | links: 6 | - 7 | text: gulp-uncss 8 | type: official 9 | url: https://www.npmjs.com/package/gulp-uncss 10 | - 11 | text: slimming Bootstrap with gulp-uncss 12 | type: blog 13 | url: https://fourword.fourkitchens.com/article/use-gulp-and-uncss-slim-down-your-css-framework 14 | --- 15 | 16 | Helps identify and isolate CSS that is **unused by your page**. By removing this unnecessary CSS you can reduce the size of the code delivered to your users, while not having to manually strip out pieces of a framework or library that you've chosen to use. 17 | -------------------------------------------------------------------------------- /_workflow/8-fontfaceobserver.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: fontfaceobserver 3 | type: tool 4 | 5 | links: 6 | - 7 | text: fontfaceobserver 8 | type: official 9 | url: https://www.npmjs.com/package/fontfaceobserver 10 | - 11 | text: webfonts and performance 12 | type: blog 13 | url: http://fourword.fourkitchens.com/article/webfonts-and-performance 14 | --- 15 | 16 | Allows you to have control over how your text displays when being enhanced by a webfont. Keep the FOUT and avoid any sign of FOIT with a font observer. 17 | 18 | -------------------------------------------------------------------------------- /_workflow/90-activity-css.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CSS optimization 3 | type: activity 4 | 5 | tasks: 6 | - 7 | id: automation-css-bootstrap 8 | text: Get rid of CDN-hosted Bootstrap. Combine bootstrap files with the custom Sass the site already has. 9 | - 10 | id: automation-css-uncss 11 | text: Remove unused parts of bootstrap from the combined files. 12 | - 13 | id: automation-css-minify 14 | text: Minify CSS. 15 | - 16 | id: automation-css-critical 17 | text: Generate critical CSS and inline it. 18 | - 19 | id: automation-css-async 20 | text: Asynchronously load the full CSS file using loadCSS. 21 | - 22 | id: automation-css-critical-workflow 23 | text: Generate critical CSS and inline it as part of the CSS workflow. 24 | --- 25 | -------------------------------------------------------------------------------- /_workflow/91-activity-fonts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Font optimization 3 | type: activity 4 | 5 | tasks: 6 | - 7 | id: automation-fonts-async 8 | text: Load fonts asynchronously 9 | - 10 | id: automation-fonts-observer 11 | text: Use a font observer to completely aoid any FOIT. The FOUT should remain. 12 | --- 13 | -------------------------------------------------------------------------------- /_workflow/91-activity-js.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: JS optimization 3 | type: activity 4 | 5 | tasks: 6 | - 7 | id: automation-js-combine 8 | text: Combine/minify JS 9 | - 10 | id: automation-js-async 11 | text: Use the async attribute to load JS asynchronously. 12 | --- 13 | -------------------------------------------------------------------------------- /about.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: About this Training 3 | --- 4 | 5 |
6 |

About this Training

7 |
8 | 9 | 12 | 13 |
14 |
15 |

Create a foundation of knowledge

16 |
17 |

We will walk through the process of optimizing an existing site. Learn how to identify and measure performance bottlenecks, and respond with solutions to these problems.

18 |
19 |
20 |
21 |

Use automated workflow tools

22 |
23 |

Tools can tirelessly check every change you make to a site, ensuring that major changes in performance do not go unnoticed during the development cycle.

24 |

Use automation to always deploy an optimized build, using an asset pipeline that ensures your HTML, CSS, JS, and images are all as lean as possible.

25 |
26 |
27 |
28 |

Confidently build performant sites

29 |
30 |

Build sites from scratch while balancing other priorities like feature backlogs and deadlines through the use of performance budgets.

31 |
32 |
33 |
34 | 35 | 38 | 39 |
40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 |
8:00 - 9:00Breakfast / Registration
9:00 - 9:45Foundation of Performance
9:45 - 10:15Performance Auditing
10:15 - 10:45Coffee break
10:45 - 11:15Performance Auditing (cont.)
10:45 - 11:45Drupal Modules
11:45 - 1:00Lunch
1:00 - 1:45Workflow Automation
1:45 - 3:15Continuous Integration
3:15 - 3:45Coffee break
3:45 - 4:30Performance Budgeting
4:30 - 5:00Wrap up / Evaluations
5:00 - 6:30Opening reception in the Exhibit Hall
94 |
95 | -------------------------------------------------------------------------------- /audit.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Auditing Performance 3 | --- 4 | 5 |

Auditing Performance

6 | 7 | 10 | 11 |
12 |

We are most familiar with Chrome and so all of the browser examples will follow suit. However, most of the tools demoed are available in all browsers.

13 |
14 | 15 |

Every browser has a powerful set of development tools (DevTools) lurking inside, waiting for you to unlock its full potential. Every type of browser performance problem, from page loads to animation jank, can be debugged directly in the browser. We will cover the most oft-used tools.

16 |
17 | 18 |
19 | {% for tool in site.audit %} 20 | {% if tool.type == 'devtool' %}{% include list-tools.html %}{% endif %} 21 | {% endfor %} 22 |
23 | 24 | 27 | 28 |

These tools are hosted services, meaning you don't have to rely on your personal internet connection to perform an audit. Some have the ability to throttle the connection, or choose where in the world the test is run. They all have APIs and tests can be run as part of a workflow.

29 | 30 |
31 | {% for tool in site.audit %} 32 | {% if tool.type == 'service' %}{% include list-tools.html %}{% endif %} 33 | {% endfor %} 34 |
35 | 36 | 39 | 40 | {% include goals.html %} 41 | 42 | {% for activity in site.audit %} 43 | {% if activity.type == 'activity' %}{% include list-tasks.html %}{% endif %} 44 | {% endfor %} 45 | -------------------------------------------------------------------------------- /budgets.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Performance Budgets 3 | --- 4 | 5 |
6 |

Performance Budgets

7 |
8 | 9 |
10 |

Note about Performance Budgets

11 |
12 |

This is just as much a shift in workplace culture as it is process. Sometimes your client or product owner cannot or will not prioritize performance, so even with a warning system in place, you might not be afforded the time to fix it.

13 | Watch video about Performance Budgets 14 |
15 |
16 | 17 |

The main difficulty most teams overcome is actually putting performance front and center during a normal workflow. Many problems are much harder to solve after weeks of work have been built on top of them. One way of finding problems early and often is the use of a performance budget.

18 |

Just like a monetary budget, a performance budget places limits on your site, and as you develop features the budget keeps the site in check.

19 |

20 | Performance budget metrics 21 | How to make a performance budget 22 |

23 | 24 | 25 | 28 | 29 |

Time limits are a simple but effective metric that can be measured. Most any event can be listened for in the browser, and there are even whole Web APIs dedicated to performance timing.

30 |

Requests are a simple thing to measure but might not always tell the whole story. For instance, you could place a limit of four CSS requests, but if one of them has 250K of inline base64 data added during development, it will not trigger a warning as long as you still only have four CSS files.

31 |

File size of your page as a whole is always something good to keep in mind, especially since most mobile data plans have download limits. A heavy site costs your users money! WebPageTest includes a $ indicator on every results page letting you see an estimate of how much a single page load of your site costs in various countries.

32 |

Milestones are specific to your website or app. For example, Twitter has a "time to first tweet" milestone, meaning the measure performance based on how soon the user can read content.

33 |

34 | Speed Index is a metric invented by WebPageTest.org, and combines many factors together which measure "visual completeness" and are able to factor in things like FOUT, giving better scores to sites that are progressively enhanced to be flexible and fault tolerant.

35 | Checking the Speed Index of various devices gives one of the most reliable indicators of real-world performance. It is also quick to reveal the performance impact of common sources of bloat, such as a slideshow at the top of a homepage. For that reason it can be a very effective bargaining tool during design or product discussions. 36 |

37 |

Read Speed Index docs

38 | 39 | 40 | 43 | 44 |
45 | {% for tool in site.budgets %} 46 | {% if tool.type == 'tool' %}{% include list-tools.html %}{% endif %} 47 | {% endfor %} 48 |
49 | 50 |
51 |

Find More Tools

52 |

You can see a large collection of curated tools, articles, and tutorials at perf-tooling.today

53 |
54 | 55 | 58 | 59 | {% include goals-codebase.html %} 60 | 61 | {% for activity in site.budgets %} 62 | {% if activity.type == 'activity' %}{% include list-tasks.html %}{% endif %} 63 | {% endfor %} 64 | -------------------------------------------------------------------------------- /config.rb: -------------------------------------------------------------------------------- 1 | 2 | # Require any additional compass plugins above this line. 3 | 4 | # Set this to the root of your project when deployed: 5 | http_path = "/" 6 | css_dir = "css" 7 | sass_dir = "_sass" 8 | images_dir = "img" 9 | javascripts_dir = "js" 10 | fonts_dir = "fonts" 11 | 12 | # You can select your preferred output style here (can be overridden via the command line): 13 | # output_style = :expanded or :nested or :compact or :compressed 14 | 15 | # To enable relative paths to assets via compass helper functions. Uncomment: 16 | # relative_assets = true 17 | 18 | # To disable debugging comments that display the original location of your selectors. Uncomment: 19 | line_comments = false 20 | 21 | # Disable cache busting on image assets 22 | asset_cache_buster :none 23 | -------------------------------------------------------------------------------- /css/.gitkeep: -------------------------------------------------------------------------------- 1 | # Gulp will generate CSS in this directory 2 | -------------------------------------------------------------------------------- /drupal.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Drupal modules 3 | --- 4 | 5 |

Drupal modules

6 | 7 |

Drupal offers a number of modules specifically designed to improve frontend performance. Combined with a well-made theme, these modules can help your Drupal sites fly!

8 | 9 |
10 | {% for tool in site.drupal %} 11 | {% if tool.type == 'tool' %}{% include list-tools.html %}{% endif %} 12 | {% endfor %} 13 |
14 | 15 | 18 | 19 | {% include goals.html %} 20 | 21 | {% for activity in site.drupal %} 22 | {% if activity.type == 'activity' %}{% include list-tasks.html %}{% endif %} 23 | {% endfor %} 24 | -------------------------------------------------------------------------------- /examples/critical/critical.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script is extremely small because we're looking to avoid the use of 4 | # particular features of CSS. So a successful test will mean that we see a 5 | # return value of 0. Since 0 is the standard exit status for a bash script, 6 | # all we have to do is return the number of results found from grep. 7 | 8 | # The commands look for 'face' or 'url' within the critical CSS. Both of these 9 | # properties will cause the browser to make an HTTP request, but since the 10 | # critical CSS is supposed to be small and inline, it should *always* avoid 11 | # making external calls. External calls completely defeat the purpose of 12 | # inlining CSS in your HTML. 13 | 14 | FACE=`grep -c 'font-face' _includes/critical.css` 15 | URL=`grep -c 'url' _includes/critical.css` 16 | 17 | if [ "$FACE" == "0" ] && [ "$URL" == "0" ]; then 18 | echo "Critical: ✔︎ Yay! The generated CSS makes zero external requests." 19 | exit 0 20 | else 21 | TOTAL=$(($FACE + $URL)) 22 | echo "Critical: ✘ Rats! The generated CSS makes $TOTAL external requests." 23 | exit $TOTAL 24 | fi 25 | -------------------------------------------------------------------------------- /examples/grunt-perfbudget/Gruntfile.js: -------------------------------------------------------------------------------- 1 | // The settings var includes your WebPageTest API Key. To use yours, first go to 2 | // the following URL and follow the instructions to get a key: 3 | // 4 | // @see http://www.webpagetest.org/forums/showthread.php?tid=466 5 | // 6 | // Now make a file called settings.json in the same dir as this 7 | // Gruntfile and include an object like this: 8 | // 9 | // { 10 | // "key": "your-web-page-test-api-key" 11 | // } 12 | var settings = require('./settings.json'); 13 | 14 | // This is the normal Gruntfile 15 | module.exports = function(grunt) { 16 | grunt.initConfig({ 17 | pkg: grunt.file.readJSON('package.json'), 18 | 19 | perfbudget: { 20 | default: { 21 | // See all options here: 22 | // https://github.com/tkadlec/grunt-perfbudget#options 23 | options: { 24 | url: 'https://fourkitchens.com', 25 | key: settings.key, 26 | budget: { 27 | render: 800, 28 | requests: 12 29 | } 30 | } 31 | } 32 | } 33 | }); 34 | 35 | // Load the perfbudget plugin 36 | grunt.loadNpmTasks('grunt-perfbudget'); 37 | 38 | // Run the perfbudget task by default 39 | grunt.registerTask('default', ['perfbudget']); 40 | }; 41 | -------------------------------------------------------------------------------- /examples/grunt-perfbudget/README.md: -------------------------------------------------------------------------------- 1 | # Example of grunt-perfbudget 2 | 3 | If you don't add a README then npm complains :) 4 | -------------------------------------------------------------------------------- /examples/grunt-perfbudget/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunt_perfbudget_example", 3 | "description": "An example implementation of grunt-perfbudget.", 4 | "repository": "git@github.com:fourkitchens/frontend-perf.git", 5 | "devDependencies": { 6 | "grunt": "^0.4.5", 7 | "grunt-perfbudget": "^0.1.6" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/phantomas/phantomas--assert-requests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | LIMIT=5 3 | 4 | echo "Phantomas: limit HTTP requests to $LIMIT" 5 | phantomas --url http://localhost:4000 --assert-requests=$LIMIT >/dev/null 6 | 7 | if [ $? -eq 0 ] 8 | then 9 | echo "Phantomas: ✔︎ Yay! The site makes $LIMIT or fewer HTTP requests." 10 | exit 0 11 | else 12 | echo "Phantomas: ✘ Rats! The site makes more than $LIMIT HTTP requests." 13 | exit 1 14 | fi 15 | -------------------------------------------------------------------------------- /foundation.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Performance: why and how?" 3 | --- 4 | 5 |
6 |

Performance: why and how?

7 |
8 | 9 | 12 | 13 | 19 | 20 | 23 | 24 |
25 |

Many facets of performance are improving over time. One such change is HTTP/2, and it will cause sweeping changes to how sites download and arrive onto your computer. Some of the current optimizations that are a good idea with HTTP/1.1 will actually hurt your site's performance using HTTP/2. We will mark any potential regressions so you can keep the future in mind.

26 |
27 | 28 | 31 | 32 |
Concatenate, minify, compress, shard.
33 | 34 |
35 | {% for tool in site.foundation %} 36 | {% include list-tools.html %} 37 | {% endfor %} 38 |
39 | 40 | 43 | 44 |
Ignore all the above advice.
45 | 46 |
47 |
48 |

HTTP/2 not common yet

49 |
50 |
51 |

While HTTP/2 is on the horizon, HTTP/1.1 will not be going away soon.

52 |

Therefore, today's training covers HTTP/1.1 techniques. These techniques will help all sites today, while HTTP/2 requires server knowledge to properly handle both types of traffic simultaneously.

53 |
54 |
55 | 56 |

HTTP/2 was created to address all of the performance workarounds described in the previous section about HTTP/1.1. So if your server is using the newer protocol, none of those workarounds are necessary.

57 |

For exmple, with HTTP/2 the server and browser can more efficiently use the connection they create between each other, and multiple files can be sent using one single connection. Thus, the gains achieved by concatenating when using HTTP1.1 is now a restriction that the server has no choice but to obey when using HTTP2.

58 |

If you're interested in reading about HTTP/2 in order to be prepared for the future, feel free to browse the official community site.

59 | 60 | Read about HTTP/2 61 | Watch a video about HTTP/2 62 | See a demo of HTTP/2 63 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | // Gulp 2 | // 3 | // This is not a normal require, because our gulp-help tool (which provides the 4 | // nice task descriptions on the command-line) requires changing the function 5 | // signature of gulp tasks to include the task description. 6 | var gulp = require('gulp-help')(require('gulp')); 7 | 8 | // Gulp / Node utilities 9 | var u = require('gulp-util'); 10 | var log = u.log; 11 | var c = u.colors; 12 | var del = require('del'); 13 | var spawn = require('child_process').spawn; 14 | var sequence = require('run-sequence'); 15 | 16 | // Basic workflow plugins 17 | var prefix = require('gulp-autoprefixer'); 18 | var sass = require('gulp-sass'); 19 | var bs = require('browser-sync'); 20 | var reload = bs.reload; 21 | 22 | // Performance workflow plugins 23 | var concat = require('gulp-concat'); 24 | var mincss = require('gulp-minify-css'); 25 | var imagemin = require('gulp-imagemin'); 26 | var uncss = require('gulp-uncss'); 27 | var uglify = require('gulp-uglify'); 28 | var critical = require('critical').stream; 29 | 30 | // Performance testing plugins 31 | var psi = require('psi'); 32 | var wpt = require('webpagetest'); 33 | var ngrok = require('ngrok'); 34 | 35 | // ----------------------------------------------------------------------------- 36 | // Remove old CSS 37 | // 38 | // This task only deletes the files generated by the 'sass' and 'css' tasks. 39 | // The 'uncss' task is slow to run and less frequently needed, so we keep the 40 | // build process fast by preserving the results of uncss. 41 | // ----------------------------------------------------------------------------- 42 | gulp.task('clean-css', false, function() { 43 | return del(['css/{all,main}*'], function (err) { 44 | if (err) { log(c.red('clean-css'), err); } 45 | else { 46 | log( 47 | c.green('clean-css'), 48 | 'deleted old stylesheets' 49 | ); 50 | } 51 | }); 52 | }); 53 | 54 | // ----------------------------------------------------------------------------- 55 | // Sass Task 56 | // 57 | // Compiles Sass and runs the CSS through autoprefixer. A separate task will 58 | // combine the compiled CSS with vendor files and minify the aggregate. 59 | // ----------------------------------------------------------------------------- 60 | gulp.task('sass', 'Compiles Sass and uses autoprefixer', function() { 61 | return gulp.src('_sass/**/*.scss') 62 | .pipe(sass({ 63 | outputStyle: 'nested', 64 | onSuccess: function(css) { 65 | var dest = css.stats.entry.split('/'); 66 | log(c.green('sass'), 'compiled to', dest[dest.length - 1]); 67 | }, 68 | onError: function(err, res) { 69 | log(c.red('Sass failed to compile')); 70 | log(c.red('> ') + err.file.split('/')[err.file.split('/').length - 1] + ' ' + c.underline('line ' + err.line) + ': ' + err.message); 71 | } 72 | })) 73 | .pipe(prefix("last 2 versions", "> 1%")) 74 | .pipe(gulp.dest('css')); 75 | }); 76 | 77 | // ----------------------------------------------------------------------------- 78 | // Combine and Minify CSS 79 | // 80 | // This task minifies all the CSS found in the css/ directory, including the 81 | // uncss-ed copies of bootstrap. The end result is a minified aggregate, ready 82 | // to be served. 83 | // ----------------------------------------------------------------------------- 84 | gulp.task('css', 'Removes old CSS, compiles Sass, combines CSS, minifies CSS', ['clean-css', 'sass'], function() { 85 | bs.notify('Running: CSS task'); 86 | 87 | return gulp.src('css/*.css') 88 | .pipe(concat('all.min.css')) 89 | .pipe(mincss()) 90 | .pipe(gulp.dest('css')) 91 | .pipe(gulp.dest('_site/css')) 92 | .pipe(reload({stream: true})); 93 | }); 94 | 95 | 96 | // ----------------------------------------------------------------------------- 97 | // UnCSS Task 98 | // 99 | // Checks the site's usage of Bootstrap and strips unused styles out. Outputs 100 | // the resulting files in the css/ directory where they will be combined and 101 | // minified by a separate task. 102 | // 103 | // Note: this task requires a local server to be running because it references 104 | // the actual compiled site to calculate the unused styles. 105 | // ----------------------------------------------------------------------------- 106 | gulp.task('uncss', 'Removes unused CSS from frameworks', function() { 107 | return gulp.src([ 108 | 'node_modules/bootstrap/dist/css/bootstrap.css', 109 | 'node_modules/bootstrap/dist/css/bootstrap-theme.css' 110 | ]) 111 | .pipe(uncss({ 112 | html: [ 113 | 'http://localhost:3000/', 114 | 'http://localhost:3000/audit/', 115 | 'http://localhost:3000/foundation/', 116 | 'http://localhost:3000/budgets/' 117 | ] 118 | })) 119 | .pipe(gulp.dest('css/')); 120 | }); 121 | 122 | // ----------------------------------------------------------------------------- 123 | // Combine and Minify JS 124 | // 125 | // Just like the CSS task, the end result of this task is a minified aggregate 126 | // of JS ready to be served. 127 | // ----------------------------------------------------------------------------- 128 | gulp.task('js', 'Combines and minifies JS', function() { 129 | return gulp.src('_js/**/*.js') 130 | .pipe(concat('all.min.js')) 131 | .pipe(uglify()) 132 | .pipe(gulp.dest('js')) 133 | .pipe(gulp.dest('_site/js')) 134 | .pipe(reload({stream: true})); 135 | }); 136 | 137 | // ----------------------------------------------------------------------------- 138 | // Generate critical-path CSS 139 | // 140 | // This task generates a small, minimal amount of your CSS based on which rules 141 | // are visible (aka "above the fold") during a page load. We will use a Jekyll 142 | // template command to inline the CSS when the site is generated. 143 | // 144 | // All styles should be directly applying to an element visible when your 145 | // website renders. If the user has to scroll even a small amount, it's not 146 | // critical CSS. 147 | // ----------------------------------------------------------------------------- 148 | gulp.task('critical', 'Generates critical CSS for the example site', function () { 149 | return gulp.src('_site/index.html') 150 | .pipe(critical({ 151 | dest: '_includes/critical.css', 152 | css: ['css/all.min.css'], 153 | dimensions: [{ 154 | width: 320, 155 | height: 480 156 | },{ 157 | width: 768, 158 | height: 1024 159 | },{ 160 | width: 1280, 161 | height: 960 162 | }], 163 | minify: true, 164 | extract: false 165 | })); 166 | }); 167 | 168 | // ----------------------------------------------------------------------------- 169 | // Minify SVGs and compress images 170 | // 171 | // It's good to maintain high-quality, uncompressed assets in your codebase. 172 | // However, it's not always appropriate to serve such high-bandwidth assets to 173 | // users, in order to reduce mobile data plan usage. 174 | // ----------------------------------------------------------------------------- 175 | gulp.task('imagemin', 'Minifies and compresses images on the example site', function() { 176 | return gulp.src('_img/**/*') 177 | .pipe(imagemin({ 178 | progressive: true, 179 | svgoPlugins: [{removeViewBox: false}] 180 | })) 181 | .pipe(gulp.dest('img')); 182 | }); 183 | 184 | // ----------------------------------------------------------------------------- 185 | // Jekyll 186 | // 187 | // Regenerate the Jekyll site when files are touched. The --watch flag is not 188 | // used because the bs/watch tasks will handle the "watching" of the files. 189 | // ----------------------------------------------------------------------------- 190 | gulp.task('jekyll', 'Development task: generate the Jekyll server', function() { 191 | bs.notify('Running: Jekyll task'); 192 | 193 | return spawn('bundle', ['exec', 'jekyll', 'build', '--config=_config.yml'], {stdio: 'inherit'}) 194 | .on('close', reload); 195 | }); 196 | 197 | // ----------------------------------------------------------------------------- 198 | // Jekyll Serve 199 | // 200 | // This command is used exclusively by Travis to start Jekyll in the background 201 | // then run tests against it. 202 | // ----------------------------------------------------------------------------- 203 | gulp.task('jekyll-serve', false, function(callback) { 204 | spawn('bundle', ['exec', 'jekyll', 'serve', '--detach', '--no-watch', '--config=_config.yml'], {stdio: 'inherit'}) 205 | .on('close', callback) 206 | }); 207 | 208 | // ----------------------------------------------------------------------------- 209 | // Browser Sync 210 | // 211 | // Makes web development better by eliminating the need to refresh. Essential 212 | // for CSS development and multi-device testing. 213 | // ----------------------------------------------------------------------------- 214 | gulp.task('browser-sync', false, function() { 215 | bs({ 216 | server: './_site/' 217 | }); 218 | }); 219 | 220 | // ----------------------------------------------------------------------------- 221 | // Browser Sync using Proxy server 222 | // 223 | // This is a second (unused) example to demonstrate how you'd connect to a local 224 | // server that runs itself. Examples would be a PHP site such as Wordpress or a 225 | // Drupal site, or a node.js site like Express. 226 | // 227 | // Usage: gulp browser-sync-proxy --port 8080 228 | // ----------------------------------------------------------------------------- 229 | gulp.task('browser-sync-proxy', false, function () { 230 | bs({ 231 | // Point this to your pre-existing server. 232 | proxy: 'my-local-site.dev' + (u.env.port ? ':' + u.env.port : ''), 233 | // This tells BrowserSync to auto-open a tab once it boots. 234 | open: true 235 | }, function(err, bs) { 236 | if (err) { 237 | console.log(bs.options); 238 | } 239 | }); 240 | }); 241 | 242 | 243 | // ----------------------------------------------------------------------------- 244 | // Watch tasks 245 | // 246 | // These tasks are run whenever a file is saved. Don't confuse the files being 247 | // watched (gulp.watch blobs in this task) with the files actually operated on 248 | // by the gulp.src blobs in each individual task. 249 | // 250 | // A few of the performance-related tasks are excluded because they can take a 251 | // bit of time to run and don't need to happen on every file change. If you want 252 | // to run those tasks more frequently, set up a new watch task here. 253 | // ----------------------------------------------------------------------------- 254 | gulp.task('watch', 'Watch for changes to various files and process them', function() { 255 | gulp.watch('_sass/**/*.scss', ['css']); 256 | gulp.watch('_js/**/*.js', ['js']); 257 | gulp.watch('_img/**/*', ['imagemin']); 258 | gulp.watch(['./**/*.{md,html}', '!./_site/**/*.*', '!./node_modules/**/*.*'], ['jekyll']); 259 | }); 260 | 261 | // ----------------------------------------------------------------------------- 262 | // Convenience task for development. 263 | // 264 | // This is the command you run to warm the site up for development. It will do 265 | // a full build, open BrowserSync, and start listening for changes. 266 | // ----------------------------------------------------------------------------- 267 | gulp.task('bs', 'Main development task:', ['css', 'js', /*'imagemin',*/ 'jekyll', 'browser-sync', 'watch']); 268 | 269 | // ----------------------------------------------------------------------------- 270 | // Performance test: Phantomas 271 | // 272 | // Phantomas can be used to test granular performance metrics. This example 273 | // ensures that the site never exceeds a specific number of HTTP requests. 274 | // ----------------------------------------------------------------------------- 275 | gulp.task('phantomas', 'Performance: measure a URL using Phantomas', function() { 276 | var limit = 5; 277 | var phantomas = spawn('./node_modules/.bin/phantomas', ['--url', 'http://localhost:4000', '--assert-requests=' + limit]); 278 | 279 | // Uncomment this block to see the full Phantomas output. 280 | // phantomas.stdout.on('data', function (data) { 281 | // data = data.toString().slice(0, -1); 282 | // log('Phantomas:', data); 283 | // }); 284 | 285 | // Catch any errors. 286 | phantomas.on('error', function (err) { 287 | log(err); 288 | }); 289 | 290 | // Log results to console. 291 | phantomas.on('close', function (code) { 292 | // Exit status of 0 means success! 293 | if (code === 0) { 294 | log('Phantomas:', c.green('✔︎ Yay! The site makes ' + limit + ' or fewer HTTP requests.')); 295 | } 296 | 297 | // Exit status of 1 means the site failed the test. 298 | else if (code === 1) { 299 | log('Phantomas:', c.red('✘ Rats! The site makes more than ' + limit + ' HTTP requests.')); 300 | process.exit(code); 301 | } 302 | 303 | // Other exit codes indicate problems with the test itself, not a failed test. 304 | else { 305 | log('Phantomas:', c.bgRed('', c.black('Something went wrong. Exit code'), code, '')); 306 | process.exit(code); 307 | } 308 | }); 309 | }); 310 | 311 | // ----------------------------------------------------------------------------- 312 | // Performance test: PageSpeed Insights 313 | // 314 | // Initializes a public tunnel so the PageSpeed service can access your local 315 | // site, then it tests the site. This task outputs the standard PageSpeed results. 316 | // 317 | // The task will output a standard exit code based on the result of the PSI test 318 | // results. 0 is success and any other number is a failure. To learn more about 319 | // bash-compatible exit status codes read this page: 320 | // 321 | // http://tldp.org/LDP/abs/html/exit-status.html 322 | // ----------------------------------------------------------------------------- 323 | gulp.task('psi', 'Performance: PageSpeed Insights', function() { 324 | // Set up a public tunnel so PageSpeed can see the local site. 325 | return ngrok.connect(4000, function (err_ngrok, url) { 326 | log(c.cyan('ngrok'), '- serving your site from', c.yellow(url)); 327 | 328 | // Run PageSpeed once the tunnel is up. 329 | psi.output(url, { 330 | strategy: 'mobile', 331 | threshold: 80 332 | }, function (err_psi, data) { 333 | // Log any potential errors and return a FAILURE. 334 | if (err_psi) { 335 | log(err_psi); 336 | process.exit(1); 337 | } 338 | 339 | // Kill the ngrok tunnel and return SUCCESS. 340 | process.exit(0); 341 | }); 342 | }); 343 | }); 344 | 345 | // ----------------------------------------------------------------------------- 346 | // Performance test: Critical CSS 347 | // 348 | // This test checks our generated critical CSS to make sure there are no external 349 | // requests which would block rendering. Having external calls defeats the entire 350 | // purpose of inlining the CSS, since the external call blocks rendering. 351 | // ----------------------------------------------------------------------------- 352 | gulp.task('critical-test', 'Performance: check the contents of critical CSS', function () { 353 | // Spawn our critical CSS test and suppress all output. 354 | var critical = spawn('./examples/critical/critical.sh', ['>/dev/null']); 355 | 356 | // Catch any errors. 357 | critical.on('error', function (err) { 358 | log(err); 359 | }); 360 | 361 | // Log results to console. 362 | critical.on('close', function (code) { 363 | // Exit status of 0 means success! 364 | if (code === 0) { 365 | log('Critical:', c.green('✔︎ Yay! The generated CSS makes zero external requests.')); 366 | } 367 | 368 | // Exit status of anything else means the test failed. 369 | else { 370 | log('Critical:', c.red('✘ Rats! The generated CSS makes ' + code + ' external requests.')); 371 | process.exit(code); 372 | } 373 | }); 374 | }); 375 | 376 | // ----------------------------------------------------------------------------- 377 | // Performance test: WebPageTest.org 378 | // 379 | // Initializes a public tunnel so the PageSpeed service can access your local 380 | // site, then it tests the site. This task outputs the standard PageSpeed results. 381 | // ----------------------------------------------------------------------------- 382 | gulp.task('wpt', 'Performance: WebPageTest.org', function () { 383 | var wpt_test = wpt('www.webpagetest.org', process.env.wptkey); 384 | 385 | // Set up a public tunnel so WebPageTest can see the local site. 386 | return ngrok.connect(4000, function (err_ngrok, url) { 387 | log(c.cyan('ngrok'), '- serving your site from', c.yellow(url)); 388 | 389 | // The `url` variable was supplied by ngrok. 390 | wpt_test.runTest(url, function(err_wpt, data_wpt) { 391 | // Log any potential errors and return a FAILURE. 392 | if (err_wpt) { 393 | log(err_wpt); 394 | process.exit(1); 395 | } 396 | 397 | // Open window to results. 398 | var wpt_results = 'http://www.webpagetest.org/result/' + data_wpt.data.testId; 399 | log(c.green('✔︎ Opening results page:', wpt_results)); 400 | spawn('open', [wpt_results]); 401 | 402 | // Note to developer. 403 | log(c.yellow('⚠️ Please keep this process running until WPT is finished.')); 404 | log(c.yellow('⚠️ Once the results load, hit Control + C to kill this process.')); 405 | }); 406 | }); 407 | }); 408 | 409 | // ----------------------------------------------------------------------------- 410 | // Performance test: run everything at once 411 | // 412 | // Having a task that simply runs other tasks is nice for Travis or other CI 413 | // solutions, because you only have to specify one command, and gulp handles 414 | // the rest. 415 | // ----------------------------------------------------------------------------- 416 | gulp.task('test', 'Run all tests', function (callback) { 417 | sequence( 418 | 'jekyll-serve', 419 | 'critical-test', 420 | 'phantomas', 421 | 'psi', 422 | 'wpt', 423 | callback 424 | ); 425 | }); 426 | 427 | // ----------------------------------------------------------------------------- 428 | // Default: load task listing 429 | // 430 | // Instead of launching some unspecified build process when someone innocently 431 | // types `gulp` into the command line, we provide a task listing so they know 432 | // what options they have without digging into the file. 433 | // ----------------------------------------------------------------------------- 434 | gulp.task('default', false, ['help']); 435 | -------------------------------------------------------------------------------- /img/.gitkeep: -------------------------------------------------------------------------------- 1 | # Gulp will generate minified/compressed images in this directory. 2 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Welcome 3 | --- 4 | 5 |
6 |

Frontend Performance Training

7 |
8 | 9 |
10 |

Wifi: Drupal

11 |

Pass: nola2016

12 |
13 | 14 |
15 |

GitHub: https://github.com/fourkitchens/frontend-perf

16 |
17 | 18 |

19 | 20 | 23 | 24 |
25 |
26 |
Taylor Smith
27 |
Taylor Smith

Taylor Smith is a graphic designer and frontend developer with a passion for the clean code, strong typography, great design, and other curiosities.
28 |
29 |
30 |
Luke Herrington
31 |
Luke Herrington

Luke is a Javascript developer who recently leveled up his Sass/Frontend Drupal skills. He enjoys learning new concepts and skills and then applying them. He does not enjoy writing bios.
32 |
33 |
34 | -------------------------------------------------------------------------------- /js/.gitkeep: -------------------------------------------------------------------------------- 1 | # Gulp will generate JS in this directory 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend-perf-training", 3 | "repository": "git@github.com:fourkitchens/frontend-perf.git", 4 | "license": "MIT", 5 | "dependencies": { 6 | "fontfaceobserver": "^1.7.1" 7 | }, 8 | "devDependencies": { 9 | "bootstrap": "^3.3.4", 10 | "browser-sync": "^2.6.9", 11 | "critical": "^0.7.2", 12 | "del": "^1.1.1", 13 | "ghooks": "^0.3.2", 14 | "gulp": "^3.9.0", 15 | "gulp-autoprefixer": "^2.1.0", 16 | "gulp-concat": "^2.5.2", 17 | "gulp-exit": "0.0.2", 18 | "gulp-help": "^1.6.1", 19 | "gulp-imagemin": "^2.3.0", 20 | "gulp-minify-css": "^1.0.0", 21 | "gulp-sass": "^1.3.3", 22 | "gulp-uglify": "^1.2.0", 23 | "gulp-uncss": "^1.0.1", 24 | "gulp-util": "^3.0.4", 25 | "ngrok": "^2.1.8", 26 | "phantomas": "^1.10.0", 27 | "psi": "^1.0.6", 28 | "run-sequence": "^1.1.0", 29 | "webpagetest": "^0.3.3" 30 | }, 31 | "config": { 32 | "ghooks": { 33 | "pre-push": "node_modules/.bin/gulp test" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /workflow.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Workflow Automation 3 | --- 4 | 5 |

Workflow Automation

6 | 7 |
8 |
9 |

Using within Drupal

10 |
11 |
12 |

Many people place these tools in the primary theme folder of their site. However, it is often more useful to put them in the root of your repo, so that both custom modules and themes can share the same workflow, coding standards, and so forth.

13 |
14 |
15 | 16 |

Sometimes installing a few modules won't cut it, and you'll need to rely on additional tools to improve your theme's output. A theme is often the source of many images, scripts, and other things that slow the frontend down.

17 |

Using a consistent process to make sure all of your assets are always lean is a good way to control the page weight of a site.

18 | 19 |

Automation tools

20 | 21 |

With these tools you will be able to capture repetitive tasks and run them automatically whenever you need them. It might be as frequent as a file save, or only once per deploy. But either way, having an automated workflow is less error-prone and more reliable than manually remembering and executing the tasks.

22 | 23 |
24 | {% for tool in site.automation %} 25 | {% if tool.type == 'tool' %}{% include list-tools.html %}{% endif %} 26 | {% endfor %} 27 |
28 | 29 |

Optimization tools

30 | 31 |

Here is a list of tools we frequently use in our frontend workflow:

32 | 33 |
34 | {% for tool in site.workflow %} 35 | {% if tool.type == 'tool' %}{% include list-tools.html %}{% endif %} 36 | {% endfor %} 37 |
38 | 39 | 42 | 43 | {% include goals-codebase.html %} 44 | 45 | {% for activity in site.workflow %} 46 | {% if activity.type == 'activity' %}{% include list-tasks.html %}{% endif %} 47 | {% endfor %} 48 | --------------------------------------------------------------------------------