/udportfolio

Repo for website performance optimization project with Udacity

Primary LanguageJavaScript

Steps I took to complete this project

Goals:

  • Part 1: Achieve a pagespeed insights score of above 90 on index.html of the portfolio page.
  • Part 2: Optimize the function for resizing pissas on pizza.html to do so in less than 5ms.
  • Part 3: Achieve a framerate of 60fps when scrolling in pizza.html.
  • Part 4: Make this documentation.

After viewing the classroom lessons and reviewing the code, I broke this project down into the three parts above. Then I added this documentation to the end.

Part 1: 90 or above on pagespeed insights for index.html

  1. I added print media query to print css to stop its render blocking.
  2. I added async to google analytics.
  3. I changed the size of the pizzeria picture to match how it's shown on index.html instead of shrinking such a huge image. I named that image pizzeriaMin.png. I also compressed the images using ImageOptim. I replaced Cam's picture with mine and optimized it also.
  4. I'm a newbie at gulp, so this part took me a little bit to figure out. I minified inlined the CSS. This part got me to where I needed to be as far as pagespeed insights.

image

Part 2: Pizza resize in pizza.html in less than 5ms

  • Original Code
  function changePizzaSizes(size) {
    for (var i = 0; i < document.querySelectorAll(".randomPizzaContainer").length; i++) {
      var dx = determineDx(document.querySelectorAll(".randomPizzaContainer")[i], size);
      var newwidth = (document.querySelectorAll(".randomPizzaContainer")[i].offsetWidth + dx) + 'px';
      document.querySelectorAll(".randomPizzaContainer")[i].style.width = newwidth;
    }
  }

  changePizzaSizes(size);
}
  • First Changes
  function changePizzaSizes(size) {

      var dx = determineDx(document.getElementsByClassName("randomPizzaContainer")[1], size);
      var newwidth = (document.getElementsByClassName("randomPizzaContainer")[1].offsetWidth + dx) + 'px';
      // Made this new variable from what was once the start of the function
      var pizzaContainerLength = document.getElementsByClassName("randomPizzaContainer").length;
    /* ---------------------------------------------
    I moved the for loop down here in the function,
    so that it calculates the variables first
    --------------------------------------------- */
    for (var i = 0; i < pizzaContainerLength; i++) {
      document.getElementsByClassName("randomPizzaContainer")[i].style.width = newwidth;
    }
  }

  changePizzaSizes(size);
}
  • This got me to 2.44ms, well below 5ms. Then I noticed I could make it simpler:

  • More Changes

  function changePizzaSizes(size) {

      var dx = determineDx(document.getElementsByClassName("randomPizzaContainer")[1], size);
      var newwidth = (document.getElementsByClassName("randomPizzaContainer")[1].offsetWidth + dx) + 'px';
      // Made this new variable from what was once the start of the function
      var pizzaElements = document.getElementsByClassName("randomPizzaContainer");
    /* ---------------------------------------------
    I moved the for loop down here in the function,
    so that it calculates the variables first
    --------------------------------------------- */
    for (var i = 0; i < pizzaElements.length; i++) {
      pizzaElements[i].style.width = newwidth;
    }
  }

  changePizzaSizes(size);
}
  • That small change brought me from an average 2.44ms to 2.25ms

Part 3: 60fps on scroll in pizza.html

Special thanks to mcs, forum mentor on Udacity's discussion forum. He really helped me get the ball rolling on this part of the project after I read this post. Also, thanks to for posting the office hours.

  1. I moved the DOM calculations out of the for-loop within updatePositions. Also, I changed out querySelectorAll for getElementsByClassName
  • Original Code:
function updatePositions() {
  frame++;
  window.performance.mark("mark_start_frame");

  var items = document.querySelectorAll('.mover');
  for (var i = 0; i < items.length; i++) {
    var phase = Math.sin((document.body.scrollTop / 1250) + (i % 5));
    items[i].style.left = items[i].basicLeft + 100 * phase + 'px';
  }
 }
  • What I did:
/* ----------------------------------------------------------------------
 querySelectorAll is slower than getElementsByClassName, I switched them
---------------------------------------------------------------------  */

function updatePositions() {
  frame++;
  window.performance.mark("mark_start_frame");

/* --- Moved these variables out of the for-loop --- */
  var scroll = document.body.scrollTop;
  var items = document.getElementsByClassName('mover');
  var itemsLength = items.length;

  for (var i = 0; i < itemsLength; i++) {
    var phase = Math.sin((scroll / 1250) + (i % 5));
    items[i].style.left = items[i].basicLeft + 100 * phase + 'px';
  }
 }
  1. I moved styles out of the JavaScript and into the CSS .mover class
  • Original code:
document.addEventListener('DOMContentLoaded', function() {
  var cols = 8;
  var s = 256;
  for (var i = 0; i < 200; i++) {
    var elem = document.createElement('img');
    elem.className = 'mover';
    elem.src = "images/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();
}
  • My Code in main.js:
/* --------------------------------------------------------------------------------
I moved the styles code for the sizing of the pizzas to the css to the .mover class
-------------------------------------------------------------------------------- */

/* - Generates the sliding pizzas when the page loads.  */
document.addEventListener('DOMContentLoaded', function() {
  var cols = 8;
  var s = 256;
  for (var i = 0; i < 200; i++) {
    var elem = document.createElement('img');
    elem.className = 'mover';
    elem.src = "images/pizza.png";
    elem.basicLeft = (i % cols) * s;
    elem.style.top = (Math.floor(i / cols) * s) + 'px';
    document.querySelector("#movingPizzas1").appendChild(elem);
  }
  updatePositions();
}
  • style.css:
.mover {
  position: fixed;
  width: 73px;
  z-index: -1;
}
  • My results from those changes were good, but not good enough:

image

  • My attempt to optimize things further in updatepositions:
function updatePositions() {
  frame++;
  window.performance.mark("mark_start_frame");

/* --- Moved these variables out of the for-loop --- */
  var scroll = (document.body.scrollTop / 1250);
  var items = document.getElementsByClassName('mover');
  var itemsLength = items.length;

  for (var i = 0; i < itemsLength; i++) {
    var phase = Math.sin(scroll + (i % 5));
    items[i].style.left = items[i].basicLeft + 100 * phase + 'px';
  }
  • And then in addEventListener('DOMContentLoaded')
document.addEventListener('DOMContentLoaded', function() {
  var cols = 8;
  var s = 256;
  var rows = Math.floor(window.screen.height / s);
  var elementNumber = cols * rows;
  var elem = [];
  var appendElementsHere = document.querySelector("#movingPizzas1");


  for (var i = 0; i < elementNumber; i++) {
    elem = document.createElement('img');
    elem.className = 'mover';
    elem.src = "images/pizza.png";
    elem.basicLeft = (i % cols) * s;
    elem.style.top = (Math.floor(i / cols) * s) + 'px';
    appendElementsHere.appendChild(elem);
  }
  updatePositions();
});

Throwing requestAnimationFrame into the mix:

function updatePositions() {
  animating = false;
  frame++;
  window.performance.mark("mark_start_frame");

/* --- Moved these variables out of the for-loop --- */
  var scroll = (document.body.scrollTop / 1250);
  var items = document.getElementsByClassName('mover');
  var itemsLength = items.length;

  for (var i = 0; i < itemsLength; i++) {
    var phase = Math.sin(scroll + (i % 5));
    items[i].style.left = items[i].basicLeft + 100 * phase + 'px';
  }
window.addEventListener('scroll', rAf);

function rAf() {
  if(!animating) {
    requestAnimationFrame(updatePositions);
  }
  animating = true;
}
  • & also going back to the styles within the script as they were before, except I corrected the perportions of the image
/* - Generates the sliding pizzas when the page loads.  */
document.addEventListener('DOMContentLoaded', function() {
  var cols = 8;
  var s = 256;
  var rows = Math.floor(window.screen.height / s);
  var elementNumber = cols * rows;
  var elem = [];
  var appendElementsHere = document.querySelector("#movingPizzas1");


  for (var i = 0; i < elementNumber; i++) {
    elem = document.createElement('img');
    elem.className = 'mover';
    elem.src = "images/pizza.png";
    elem.style.height = "100px";
    elem.style.width = "77.333px"; // corrected the proportion of the pizza
    elem.basicLeft = (i % cols) * s;
    elem.style.top = (Math.floor(i / cols) * s) + 'px';
    appendElementsHere.appendChild(elem);
  }
  updatePositions();
});

image

  • So much for fixing all this with optimizations. Time to learn how to use Translate:
var items = []; // makes items a global value on DOMcontentLoaded
var itemsLength = []; // makes itemsLength global

function updatePositions() {
  frame++;
  window.performance.mark("mark_start_frame");

/* --- Moved these variables out of the for-loop --- */
  var scroll = document.body.scrollTop / 1250;

  for (var i = 0; i < itemsLength; i++) {
    var phase = Math.sin(scroll + (i % 5));
    /* When I originally created transX and used it to change the style to transform, it moved
     everything right and off the screen partially, I fixed that with CSS */
    var transX = items[i].basicLeft + 100 * phase + 'px';
    items[i].style.transform =  'translateX('+transX+')';
    //items[i].style.left = items[i].basicLeft + 100 * phase + 'px'; < original code I saved for experimental purposes
  }
  window.animating = false;

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

// runs updatePositions on scroll...
//window.addEventListener('scroll', updatePositions); < original code

window.addEventListener('scroll', rAf);

  function rAf() {
  if(!window.animating) {
    window.animating = true;
    requestAnimationFrame(updatePositions);
  }
}

/* - Generates the sliding pizzas when the page loads.  */
document.addEventListener('DOMContentLoaded', function() {
  var cols = 8;
  var s = 256;
  // I moved as much stuff as I could out of the for-loop
  var rows = Math.floor(window.screen.height / s); // finds the number of rows by dividing the height of the screen in pixels by the pixels size of each row of moving pizzas
  var elementNumber = cols * rows; // decides the number of elements there will be by multiplying the cols by rows
  var appendElementsHere = document.querySelector("#movingPizzas1");


  for (var i = 0; i < elementNumber; i++) {
    var elem = document.createElement('img');
    elem.className = 'mover';
    elem.src = "images/pizza.png";
    /* corrected the proportion of the pizza
     and moved it's style to CSS */
    elem.basicLeft = (i % cols) * s;
    elem.style.top = (Math.floor(i / cols) * s) + 'px';
    appendElementsHere.appendChild(elem);
  }
// I moved this from updatepositions so that they are pre-loaded global values
  items = document.getElementsByClassName('mover'); // & changed from querySelectorAll
  itemsLength = items.length;
  updatePositions();
});

Changes: style.css

.mover {
  position: fixed;
  width: 77.333px;
  height: 100px;
  left: 0;
  right: 0;
  z-index: -1;
  backface-visibility: hidden;
}

Paint flashing of the background pizza's is gone! Thanks andrew_R for this advise on how to use the dev tools, which are very different from in the lessons.

Special thanks to mcs once again for his guidance on the forums here,here, and here. To get translate to work properly I had to fidget with CSS, but it looks good and it seems to be performant to me.

Website Performance Optimization portfolio project

Your challenge, if you wish to accept it (and we sure hope you will), is to optimize this online portfolio for speed! In particular, optimize the critical rendering path and make this page render as quickly as possible by applying the techniques you've picked up in the Critical Rendering Path course.

To get started, check out the repository, inspect the code,

Getting started

Some useful tips to help you get started:

  1. Check out the repository
  2. To inspect the site on your phone, you can run a local server
$> cd /path/to/your-project-folder
$> python -m SimpleHTTPServer 8080
  1. Open a browser and visit localhost:8080
  2. Download and install ngrok to make your local server accessible remotely.
$> cd /path/to/your-project-folder
$> ngrok 8080
  1. Copy the public URL ngrok gives you and try running it through PageSpeed Insights! More on integrating ngrok, Grunt and PageSpeed.

Profile, optimize, measure... and then lather, rinse, and repeat. Good luck!

Optimization Tips and Tricks

Customization with Bootstrap

The portfolio was built on Twitter's Bootstrap framework. All custom styles are in dist/css/portfolio.css in the portfolio repo.

Sample Portfolios

Feeling uninspired by the portfolio? Here's a list of cool portfolios I found after a few minutes of Googling.