- Overview
- Getting Started w/ Grunt
- Getting Started w/ Python SimpleHTTPServer
- Optimizations for Cam's Pizzeria Scrolling
- Optimizations for Cam's Pizzeria Resize Pizzas
The project challenged students to take on two tasks:
- Optimize the portfolio page (index.html) to receive a Google Page Speed Insights Score of 90 (or more) for web and mobile.
- Optimize the Cam's Pizzeria page (views/pizza.html) to run at 60FPS or more.
This was a very intriguing project because it caused me to spend a lot of time learning about performant practices in JavaScript. If interested, please do check out some of the following resources:
This project uses Grunt as a build system. Before running any of the build process, please install Grunt first. If you haven't, you'll need to install NodeJs first. Follow these steps if you want a full walk through of the process for building the project.
- Check out this repository
- Navigate to the downloaded repository and download the dependencies
$> cd /path/to/frontend-nanodegree-mobile-portfolio/
$> npm install
- Build the project and start the server
$> grunt dist
This will build the project and start a webserver availble at port 8081. Here's what you'll see from on the terminal:
Running "connect:server" (connect) task
Waiting forever...
Started connect web server on http://localhost:8081
This confirms what the port will be for the server. Don't like this port? Want to use another? No problem!
$> grunt dist --port=your-port-here
The output of the build process is found in the dist
directory.
4. Open a browser and visit http://localhost:8081
to get started
If you have python
installed on your machine, you can boot this project very quickly as follows:
- Download the repository
- Navigate to the downloaded repository and run a local server
$> cd /path/to/frontend-nanodegree-mobile-portfolio/dist
$> python -m SimpleHTTPServer 8000
- Open a browser and visit
http:localhost: 8000
to get started
In the event listener for DOMContentLoaded
the code was updated to no longer arbitrarily create 200 pizzas for the background. That would result in 1 DOM node per pizza. To optimize this the dimensions of the viewport are calculated and a grid of pizzas is created based on the dimensions. This way we aren't creating pizzas for space that won't even get rendered.
var numRows = Math.ceil(window.innerHeight / s);
var numCols = Math.ceil(window.innerWidth / s);
// Use this nested loop to create rows of pizzas
// and create pizzas for the UI
for (var i =0; i < numRows; i++) {
for (var j = 0; j < numCols; j++) {
createPizza(i, j);
}
}
The createPizza
function helps with readability but not with any specific optimizations there. Another small change here is that at the end of this callback, we request update the positions of the pizzas during an animation frame provided by the browser.
The scrolling animation iterates through the background pizzas and then based on a formula (sinusoidal phases) moves the pizzas laterally. This function had lots of changes to get the animation to be smooth and less costly.
The first step was to not have the updatePositions
function be called on ever event generated by the scroll listener. To accomplish this the event listener calls a function to request an animation frame. If the frame is currently in the middle of processing the last call to updatePositions
the additional call isn't made. Here's the code for that:
// We register the event listener
window.addEventListener("scroll", requestFrame, false);
// When called, we only request an animationFrame IF we aren't in the middle of working on the last request
function requestFrame() {
if (!isInFrame) {
window.requestAnimationFrame(updatePositions);
isInFrame = true;
lastY = window.scrollY;
}
}
Now we don't bombard the updatePositions
function and we get a somewhat smoother experience. But we can do better.
In the updatePositions
function there are a few values that once were calculated each execution that won't change (or not likely to change) between executions. We make some changes to cache those values where appropriate:
function updatePositions() {
var distance;
var phases = [];
...
var pizzas = document.getElementsByClassName("mover");
// Cache the phases, they only change based on the scroll distance
for (var j = 0; j < 5; j++) {
phases.push(Math.sin((lastY / 1250) + j));
}
...
}
As can be seen above in the loop the phases are cached before the values are used in an positioning lines of code. Remember lastY
from the requestFrame
function? Here's where we use it instead of making calls to document.body.scrollTop
and asking the browser to do layout related calculations. This actually saves us a layout step in the timeline. But, as before, we can do better.
Updating the CSS from JavaScript is going to have a performance impact but we can make some changes to the way the pizzas are animated to mitigate that impact. The original code performed the moves via:
var items = document.querySelectorAll('.mover');
for (var i = 0; i < items.length; i++) {
...
items[i].style.left = items[i].basicLeft + 100 * phase + 'px';
^^^^
}
This isn't wrong, but in a step to optimize we can use hardware accelerated 3d transforms and move the pizzas to their own layers. One of the impacts this change has on the pipeline is that it transforms only invoke composite events (learn more). Another step was to hint to the browser that we'll be doing transformations. This may encourage the browser to do some optimizations by creating individual layers for the DOM elements that are hinted to be adjusted this way. Here's how we do it in the pizza creation step:
var elem = document.createElement("img");
...
elem.style.willChange = "transform";
elem.style.transform = "translateZ(0)";
...
movingPizzas.appendChild(elem);
With the combined effort of caching layout calculating calls and 3d transforms, we're able to optimize the scroll animation to run as smooth as butter!
The optimizations for making the pizza resizing were smaller but are still worth taking the time to explore.
The original resizePizzas
had a function named determineDx
to help figure out what the new pixel size would be. It queried the the DOM for widths of elements which, as we've seen, can trigger layout calculations. The change here was to remove the function altogether and simple use the percentages returned from sizeSwitcher
to get the new size.
function changePizzaSizes(size) {
...
for (var i = 0; i < numPizzaContainerElements; i++) {
pizzaContainerList[i].style.width = (sizeSwitcher(size) * 100) + "%";
}
}
There is still an update to the CSS but we can live with that. One optimization I considered here would have been to use the scale
transform and not adjust the width. One of the drawbacks of that approach is that using 3d transforms on these elements would have put them onto new layers as well and that could have had an impact of performance.
Throughout the application there are small changes to things like which query selector is used and removing DOM querying from for loops wherever it made sense. I put a bit of effort to get the scrolling animation to run at 60FPS on mobile but I ran out of inspiration to get the resize down below 5ms on mobile. Perhaps this could be something fun to do as a future enhancement!
Thanks for taking the time to read this this README
- stay awesome!