Three Easy Ways to Improve Web Performance
Increasing efficiency by utilizing Code Splitting, the PRPL pattern, and Lazy Loading
A couple of weeks ago I attended a meetup and had the chance to learn about some of the incredible Google Dev Tools available for use via Google Chrome extensions, the command line, or Node modules. One of my favorite of these tools is Google Lighthouse, which conducts audits on a web page and generates a report detailing performance, accessibility, PWA, and more. It also provides specific details as to what factors played into a particular score, as well as clear suggestions with documentation for how the score could be improved.
Prior to a recent job interview, I ran Google Lighthouse on the company’s website to check out their performance (in hopes of scoring extra brownie points!). Turns out they scored a 27 out of a possible 100 in website performance, which brings them into the red zone. Big OOF!
Upon reviewing the report, we could see the main culprits were:
- Main-thread work
- Enormous network payloads
So how can we reduce the size and time of main-thread work? Let’s go through a few options.
Three different ways of code-splitting:
- Vendor splitting is separating anything in your code that you can consider a “dependency” or a third party source into a separate folder, conventionally “/vendor”. If there are any changes made to your app code or vendor code, they can be handled separately without disrupting the other.
- Entry point splitting is recommended for applications that are not distinctly set up with server-side routing and client-side routing. This means splitting code upon the initial build of a dependency using tools like webpack.
- Dynamic splitting is recommended for single page applications wherever dynamic “import()” statements are used.
It may seem like an ordeal to reconstruct your entire code base to implement code-splitting, but the good news is that there are plenty of tools available for to make the process automatic (Preact CLI, PWA Starter Kit, etc.). But if you are working on a smaller feature, project, or just starting out, know that manual code-splitting is supported by React, Vue, and Angular.
- Push critical resources for the initial URL route.
- Render the initial route.
- Pre-cache remaining routes.
- Lazy-load and create remaining routes on demand.
It is good practice to construct your code’s architecture such that you first send the minimal amount of code needed to display the page. Essentially, start by sending the skeleton, and then add the muscles, organs, and clothes later. This will significantly improve your time to interactivity. Developers devised the PRPL pattern to reduce load time and keep memory usage to a minimum for the mobile web.
The Google documentation explains PRPL effectively and efficiently, but to give you a quick rundown, PRPL is best used with the following app structure:
- the entry point: index.html — should be considerably small to minimize RAM usage
- the shell: top-level app-logic, routing, main UI, static dependencies
- fragments: anything not immediately visible at DOM Content Loaded. This is where lazy-loading comes into play.
You could also customize your unbundled build process and make use of HTTP/2 Push or
<link rel="preload"> to indicate which code snippets are essential to the skeleton framework. Where those features are not supported, you can bundle your build process into shell and fragment bundles.
Other ways of reducing payload include minifying JS, HTML, and CSS by using compressors, using text compression such as GZIP, and choosing optimal image file types and compression levels.
Lazy loading is what it sounds like — it will be ready to load, but at a later time when you need it. A quick way to save data and processing time is by deferring any resources until they come into the viewport. For images, you can use event handlers such as “scroll” or “resize”, but on more modern browsers, the intersection observer API is available for use.
In both cases, you are specifying some kind of indicator to let the code know when a resource is in the viewport. You can declare a “lazy” image url, and the actual image url, simply by giving your image tag a class of “lazy”; for background images for divs, use classes “.lazy-background” and “.lazy-background.visible”. As expected, these lazy loading libraries exist to help accelerate the implementation of lazy loading, so you do not have to thoroughly investigate what goes on behind the scenes. Don’t you love how developers help make each others’ lives easier?
The PRPL pattern is the golden standard, especially for mobile development to minimize network payload and efficiently organize your code architecture. It pretty much implies that one should apply code-splitting and lazy loading for best practice. Code-splitting is wonderful for breaking up your code into manageable pieces for the execution environment to handle, thus decluttering the main-thread work. Lazy loading can save memory and reduce load time by calling upon fragments of resources, especially media file types, only when they are needed. With these three simple implementations, you can significantly reduce your time to interactivity, and therefore create a much better user experience.
Only send the code that your users need.
Minify your code.
Compress your code.
Remove unused code.
Cache your code to reduce network trips.