A step-by-step rundown of optimizations made to a website with a number of optimization and performance-related issues, so that it achieves a high PageSpeed Insights score and runs at 60 frames per second.
$ git clone https://github.com/mikejoyceio/website-optimization
$ python -m SimpleHTTPServer
Detailed Python Simple Server instructions can been found here.
$ open "http://localhost:8000"
Contains development CSS, JS and images, sorted into corresponding directories.
Contains the production ready CSS, JS and images built from the app/ directory.
Contains the HTML for the pizza and individual project pages.
The Brunch HTML5 build tool is used to concatenate and minify scripts and style sheets in this project.
$ yarn install
$ brunch watch --server
$ brunch build --production
Detailed documentation can be found here.
The index page originally had a Google PageSpeed score of 35/100 for mobile and 47/100 for desktop. After making changes the score increased to 99/100 for both mobile and desktop. Interestingly enough, the only thing that is preventing a score of 100/100 is Google's own analytics script.
The following changes were made:
Inlined all of the CSS into the head of the document and added the HTML media="print" attribute to the external style sheet link for print styles.
Added the HTML async attribute to all script tags and used the Brunch build tool to concatenate and minify.
Resized images that were too large and compressed all images with the Kraken image compression tool.
Enabled the mod_deflate (gzip) Apache module on the server.
Leveraged browser caching by including an .htaccess file in the root of the website. The file contains expires headers, which sets long expiration times for all CSS, JavaScript and images.
The following changes where made to fix the low FPS and produce a consistent 60FPS frame rate when scrolling the page:
Renamed the mis-named 'noise' value in the global adjectives array literal to ‘noisy’ to match the switch case ‘noisy’ in the getAdj function.
var adjectives = ["dark", "color", "whimsical", "shiny", "noisy", "apocalyptic", "insulting", "praise", "scientific"];`
Optimized the loops contained in the updatePositions function and the onDOMContentLoaded event handler.
function updatePositions() {
frame++;
window.performance.mark("mark_start_frame");
var items = document.querySelectorAll('.mover');
for (var i = items.length; i--;) {
var phase = Math.sin((document.body.scrollTop / 1250) + (i % 5));
items[i].style.left = items[i].basicLeft + 100 * phase + 'px';
}
// User Timing API to the rescue again. Seriously, it's worth learning.
// Super easy to create custom metrics.
window.performance.mark("mark_end_frame");
window.performance.measure("measure_frame_duration", "mark_start_frame", "mark_end_frame");
if (frame % 10 === 0) {
var timesToUpdatePosition = window.performance.getEntriesByName("measure_frame_duration");
logAverageFrame(timesToUpdatePosition);
}
}
document.addEventListener('DOMContentLoaded', function() {
var cols = 8;
var s = 256;
for (var i = 200; i--;) {
var elem = document.createElement('img');
elem.className = 'mover';
elem.src = "../public/img/pizza.png";
elem.style.height = "100px";
elem.style.width = "73.333px";
elem.basicLeft = (i % cols) * s;
elem.style.top = (Math.floor(i / cols) * s) + 'px';
document.querySelector("#movingPizzas1").appendChild(elem);
}
updatePositions();
});
Reduced the amount of sliding pizza elements generated from 200 down to 31, which still sufficiently fills the screen with sliding pizzas.
document.addEventListener('DOMContentLoaded', function() {
var cols = 8;
var s = 256;
for (var i = 31; i--;) {
var elem = document.createElement('img');
elem.className = 'mover';
elem.src = "../public/img/pizza.png";
elem.style.height = "100px";
elem.style.width = "73.333px";
elem.basicLeft = (i % cols) * s;
elem.style.top = (Math.floor(i / cols) * s) + 'px';
document.querySelector("#movingPizzas1").appendChild(elem);
}
updatePositions();
});
Applied translateX() and translateZ(0) transform functions to the sliding pizza elements within the updatePositions function.
function updatePositions() {
frame++;
window.performance.mark("mark_start_frame");
var items = document.querySelectorAll('.mover');
for (var i = items.length; i--;) {
var phase = Math.sin((document.body.scrollTop / 1250) + (i % 5));
//items[i].style.left = items[i].basicLeft + 100 * phase + 'px';
var left = -items[i].basicLeft + 1000 * phase + 'px';
items[i].style.transform = "translateX("+left+") translateZ(0)";
}
// User Timing API to the rescue again. Seriously, it's worth learning.
// Super easy to create custom metrics.
window.performance.mark("mark_end_frame");
window.performance.measure("measure_frame_duration", "mark_start_frame", "mark_end_frame");
if (frame % 10 === 0) {
var timesToUpdatePosition = window.performance.getEntriesByName("measure_frame_duration");
logAverageFrame(timesToUpdatePosition);
}
}
Moved the calculation which utilizes the scrollTop method outside of the loop.
function updatePositions() {
frame++;
window.performance.mark("mark_start_frame");
var items = document.querySelectorAll('.mover');
var top = (document.body.scrollTop / 1250);
for (var i = items.length; i--;) {
var phase = Math.sin( top + (i % 5));
//items[i].style.left = items[i].basicLeft + 100 * phase + 'px';
var left = -items[i].basicLeft + 1000 * phase + 'px';
items[i].style.transform = "translateX("+left+") translateZ(0)";
}
// User Timing API to the rescue again. Seriously, it's worth learning.
// Super easy to create custom metrics.
window.performance.mark("mark_end_frame");
window.performance.measure("measure_frame_duration", "mark_start_frame", "mark_end_frame");
if (frame % 10 === 0) {
var timesToUpdatePosition = window.performance.getEntriesByName("measure_frame_duration");
logAverageFrame(timesToUpdatePosition);
}
}
Removed height and width styles from the generated pizza elements and resized the pizza image to 100 x 100 to prevent the browser from having to resize the images.
document.addEventListener('DOMContentLoaded', function() {
var cols = 8;
var s = 256;
for (var i = 31; i--;) {
var elem = document.createElement('img');
elem.className = 'mover';
elem.src = "../public/img/pizza-slider.png";
elem.basicLeft = (i % cols) * s;
elem.style.top = (Math.floor(i / cols) * s) + 'px';
document.querySelector("#movingPizzas1").appendChild(elem);
}
updatePositions();
});
Added the updatePositions function as a parameter to the window.requestAnimationFrame method in the scroll event listener which optimizes concurrent animations together into a single reflow and repaint cycle.
window.addEventListener('scroll', function() {
window.requestAnimationFrame(updatePositions);
});
The following changes were made to resize the pizzas in under 5ms:
Moved the determineDx function call inside the changePizzaSizes function out of the loop. Selected only the first .randomPizzaContainer in the document.
function changePizzaSizes(size) {
var dx = determineDx(document.querySelector(".randomPizzaContainer"), size);
for (var i = 0; i < document.querySelectorAll(".randomPizzaContainer").length; i++) {
var newwidth = (document.querySelectorAll(".randomPizzaContainer")[i].offsetWidth + dx) + 'px';
document.querySelectorAll(".randomPizzaContainer")[i].style.width = newwidth;
}
}
Moved the newwidth calculation inside the changePizzaSizes function out of the loop. Again, selected only the first .randomPizzaContainer element in the document.
function changePizzaSizes(size) {
var dx = determineDx(document.querySelector(".randomPizzaContainer"), size);
var newwidth = (document.querySelector(".randomPizzaContainer").offsetWidth + dx) + 'px';
for (var i = 0; i < document.querySelectorAll(".randomPizzaContainer").length; i++) {
document.querySelectorAll(".randomPizzaContainer")[i].style.width = newwidth;
}
}
Created a new variable to hold all of the .randomPizzaContainer elements in the document and looped through the elements to apply the new width value.
function changePizzaSizes(size) {
var dx = determineDx(document.querySelector(".randomPizzaContainer"), size);
var newwidth = (document.querySelector(".randomPizzaContainer").offsetWidth + dx) + 'px';
var elements = document.querySelectorAll(".randomPizzaContainer");
for (var i = 0; i < elements.length; i++) {
elements[i].style.width = newwidth;
}
}
Optimized loop inside the changePizzaSizes function.
function changePizzaSizes(size) {
var dx = determineDx(document.querySelector(".randomPizzaContainer"), size);
var newwidth = (document.querySelector(".randomPizzaContainer").offsetWidth + dx) + 'px';
var elements = document.querySelectorAll(".randomPizzaContainer");
for (var i = elements.length; i--;) {
elements[i].style.width = newwidth;
}
}
- Google Developers: Optimize CSS Delivery
- Gift of Speed: Lazy Load CSS
- Gift of Speed: Inline CSS
- Critical Path CSS Generator By Jonas Ohlsson
- Loading CSS Asynchronously By Dean Hume
- Brunch HTML5 Build Tool
- Brunch Dead Simple Skeleton
- Minify your final CSS file using Gulp.js By Ashit Vora
- Compressing your Images using Gulp.js By Ashit Vora
- Introduction to Gulp.js with practical examples By Julien Renaux
- Grunt vs Gulp vs Brunch By Andrin Riiet
- Asynchronous and deferred JavaScript execution explained By Peter Beverloo
- CSS Tricks: Async Attribute and Scripts At The Bottom
- Enable GZIP compression in WHM / cPanel with EasyApache
- Enable Apache Gzip Compression (mod_deflate) Globally in WHM/cPanel
- How to check if mod_delate in enabled
- Check GZIP Compression
- Google Developers: Cache Control
- 5 .htaccess Snippets to Borrow from HTML5 Boilerplate By David Walsh
- Jank Free
- Gone In 60 Frames Per Second: A Pinterest Paint Performance Case Study By Addy Osmani
- [VIDEO] Gone in 60fps - Making A Site Jank-Free By Addy Osmani
- Switch statement performance
- Switch vs. if else
- Writing efficient JavaScript
- O'Reilly JavaScript - The Definitive Guide - Switch
- Mozilla Developer Network: document.querySelector
- Mozilla Developer Network: document.querySelectorAll
- CSS Tricks - Transform
- Treehouse: Increase Your Site’s Performance with Hardware-Accelerated CSS
- Force Hardware Acceleration in WebKit with translate3d By David Walsh
- HTML5 Rocks: High Performance Animations By Paul Lewis and Paul Irish