Skip to content

mikejoyceio/website-optimization

Repository files navigation

Website Optimization

Project Overview

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.

Getting Started

1. Clone this repo
$ git clone https://github.com/mikejoyceio/website-optimization
2. Serve the website
$ python -m SimpleHTTPServer

Detailed Python Simple Server instructions can been found here.

3. Open the website
$ open "http://localhost:8000"

File Structure

app/

Contains development CSS, JS and images, sorted into corresponding directories.

public/

Contains the production ready CSS, JS and images built from the app/ directory.

views/

Contains the HTML for the pizza and individual project pages.

The Build

The Brunch HTML5 build tool is used to concatenate and minify scripts and style sheets in this project.

Install Brunch
$ yarn install
Develop
$ brunch watch --server
Build
$ brunch build --production

Detailed documentation can be found here.

Optimization

Index Page

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:

- CSS

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.

- JS

Added the HTML async attribute to all script tags and used the Brunch build tool to concatenate and minify.

- Images

Resized images that were too large and compressed all images with the Kraken image compression tool.

- Gzip compression

Enabled the mod_deflate (gzip) Apache module on the server.

- Browser Caching

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.

Sliding Pizzas

The following changes where made to fix the low FPS and produce a consistent 60FPS frame rate when scrolling the page:

- Fixed Typo

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 Loops

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 Pizza Elements

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();
});
- Improved CSS Animation Performance

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);
  }
}
- Improved Efficiency

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);
  }
}
- Reduced Browser Paint Events

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();
});
- Optimized Animations

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);
});

Resized Pizzas

The following changes were made to resize the pizzas in under 5ms:

- Improved Efficiency

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 Loops

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;
  }
}

Optimization Breakdown (tl;dr)

Index Page

Google PageSpeed Score before any fixes

Breakdown Image 11

Google PageSpeed Score after fixes

Breakdown Image 12

Sliding Pizzas

FPS before any fixes

Breakdown Image 01

After fixing mis-named adjectives array literal value

Breakdown Image 02

After optimizing loops

Breakdown Image 03

After reducing the number of sliding pizzas generated from 200 to 31

Breakdown Image 04

After applying translateX() and translateZ(0) transform functions to the sliding pizza elements

Breakdown Image 05

After moving a calculation utlizing the scrollTop property outside of a loop

Breakdown Image 06

After removing height & width styles from pizza image tag and resizing the image

Breakdown Image 07

After including window.requestAnimationFrame method within scroll event handler

Breakdown Image 08

Resized Pizzas

Resize time before fixes

Breakdown Image 09

Resize time after fixes

Breakdown Image 10

Resources

Index Page

Critical Fold CSS
CSS Optimization
CSS Media Queries
Build Tools
Image Optimization
JavaScript Execution
Webfonts
GZIP Compression
HTTP Caching

Pizza Page

Layout & Rendering
JavaScript Loop
JavaScript Switch
JavaScript DOM Traversal
CSS Transform
Animations
requestAnimationFrame
Chrome Devtools
Chrome Office Hours
Udacity Office Hours

About

⚡ Website Performance Optimization.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published