When I worked in e-commerce, a big metric of our engineering team was always how long it took for our average user to load our site – not just the HTML, but all assets and for the page to be completely loaded. Other similar considerations included consideration for clutter, image sizes, network traffic back and forth, and a range of other factors that were also considered. Users still expect websites to load almost instantly but my experience shows that it doesn’t happen as much as it used to.
According to Think With Google, users are more likely to bounce if a website takes longer than 1 second to load, and conversion rates drop as load times increase. Similarly, according to another study, 53% of mobile site visitors bounce if a site takes longer than 3 seconds to load. For businesses, this means optimizing your website to load in less than a second is critical.
So then, what can you do to help increase speed?
Caching HTML & Query Results
One of the most effective ways to reduce load times is by caching frequently accessed data. By utilizing in-memory caching systems like Redis and Memcached, apps can store query results or full HTML pages, allowing them to be served rapidly without repeated database hits. This drastically reduces response time and offloads work from your server’s CPU.
Key benefits include reduces the load on databases, faster retrieval of data for users, and being able to caching dynamic content for quicker responses. The difficult parts of this are figuring out a way to cache your data in a way that allows you to also invalidate either full datasets (pages, queries, etc) or fragments of those objects.
GraphQL is a API query language that allows clients to request only the data they need. Unlike traditional REST API endpoints, which may not give you all the data you need or give you too much data that you don’t need, GraphQL allows clients to request precise pieces of data in a single request. Apollo is a popular ecosystem built around GraphQL with tools like Apollo Client for frontend caching, query management, and server-side rendering. Apollo helps optimize the performance of applications by providing features such as automatic caching.
Optimizing JavaScript Asset Caching
JavaScript files are often a major contributor to slow page loads, especially when they’re updated frequently. To avoid downloading unnecessary assets, it’s important that JavaScript files are cached by the browser efficiently. This can be done by ensuring that file names or versions are not changed with every build unless necessary.
Use hashed filenames only for assets that have actually changed between builds. Set long cache lifetimes for JavaScript files that rarely change. Make sure to look at your web server to see what you’re setting for Expires
header responses. For example, with Apache Web Server, enabled expires
using a2enmod expires
and then add this extra bit into your site configuration:
<IfModule mod_expires.c>
ExpiresActive On
ExpiresDefault "access plus 1 month"
ExpiresByType image/x-icon "access plus 1 year”
ExpiresByType image/gif "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType image/jpg "access plus 1 month"
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType text/css "access 1 month”
ExpiresByType application/javascript "access plus 1 year"
</IfModule>
Minifying Assets Such As Images, CSS, JavaScript, HTML
Minification involves removing characters that don’t contribute to actual logic (like spaces, tabs, and comments) from HTML, JavaScript, and CSS assets to make them as small as possible. This minor “build” change can have a huge impact, especially on code bases that have been around for a while with a lot of comments or code bases that are highly formatted.
Use tools like Terser (for JavaScript), CSSNano (for CSS), and HTMLMinifier (for HTML) to remove bloat or roll your own simpler version using regular expressions which would involve a lot of comment removal and extra space removal.
For images, tools like TinyPNG or ImageOptim can significantly reduce file sizes without negatively affecting quality. Google’s Lighthouse does a great job of identifying images that should be optimized also.
Reducing Data Transfer Over the Network
Transferring small amounts of data over the network by optimizing responses back to the browser is another way of increasing speed. Compressing assets using Gzip (on Apache: a2enmod deflate
) or Brotli (on Apache: a2enmod brotli
) can help with latency. Using Content Delivery Networks (CDNs) to serve content closer to users can also cut down latency drastically.
Similar to the previous point of minifying assets, combining as many of your JavaScript and CSS assets into a single file to reduce network requests can absolutely help even more.
Single-Page Applications (SPAs) And Load Times
While the idea of loading a single page and a lot of rendering logic up front and updating small parts of it as required sounds efficient, many SPAs suffer from slow initial load times because of the large amount of JavaScript that needs to be downloaded and executed upfront. Popular frameworks like React.js, Vue.js, and Angular contribute to this issue because they often, but not always, require the entire code base to be loaded and initialized before the user can interact with it.
Why SPAs Can Be Slow:
- Initial Bundle Size: SPAs often have to download a large bundle of JavaScript to function, leading to a slower initial load.
- Client-Side Rendering (CSR): SPAs depend heavily on CSR, where the rendering happens after JavaScript is loaded and executed in the client’s browser. This can delay the time-to-first-paint (read initial rendering or initial display), increasing perceived load times.
- Routing and API Calls: SPAs typically handle routing on the client side, meaning that data fetching and rendering take place after the initial HTML is served, further delaying user interaction.
How to Reduce Loading Times in SPAs
While SPAs can be inherently slower due to their architecture, several techniques can help mitigate the downsides:
Code Splitting and Lazy Loading
You can take use code splitting and lazy loading with frameworks like React.js and Vue.js. Instead of delivering the entire application at once, code splitting lets you to break the app into small chunks and load what is only needed and necessary for the current view. A similar example of this is PHP and autoloading or Java’s lazy loading (though this just happens with the Java Virtual Machine).
Use React’s React.lazy()
and Vue’s dynamic imports to load components only when needed.
import React, { Suspense } from 'react';
//lazy load the component
const MyComponent = React.lazy(() => import('./MyComponent'));
function App() {
return (
<div>
<h1>App</h1>
{/* use Suspense to handle loading state */}
<Suspense fallback={<div>Loading...</div>}>
<MyComponent />
</Suspense>
</div>
);
}
export default App;
export default {
components: {
//lazy-loaded component
MyComponent: () => import('./MyComponent.vue')
},
template: `
<div>
<h1>App</h1>
<MyComponent v-if="shouldLoadComponent" />
</div>
`,
data() {
return {
shouldLoadComponent: false
};
},
methods: {
loadComponent() {
this.shouldLoadComponent = true;
}
}
};
Server-Side Rendering
Server-side rendering means that HTML for a page is generated on the server and sent to the user, reducing the initial rendering time. React’s Next.js
and Vue’s Nuxt.js
can both help with with this, allowing content to be visible quicker without waiting for the JavaScript to fully execute in the client’s browser.
Advantages include faster initial load times and it oftentimes is better (but not necessarily more) for SEO since search engines can index fully-rendered pages.
Preloading Critical Assets
Preloading critical assets like fonts, CSS, and above-the-fold images can drastically improve the perceived load time. You can instruct the browser to fetch these assets first by using <link rel="preload">
tags in your HTML.
Tree Shaking
Tree shaking helps get rid of dead code, and can significantly reduce the size of JavaScript bundles for code that has gone through several iterations of improvements and changes. Both React and Vue support tree shaking, but you’ll need to configure your build tools (like in Webpack) to ensure it is enabled and utilized.
Ensure your bundler is set up to remove unused code paths.
//webpack.config.js
module.exports = {
mode: 'production', //enables optimizations like tree shaking
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: __dirname + '/dist'
},
optimization: {
usedExports: true, //mark which exports are used, enabling tree shaking
},
};
Optimizing API Requests
For SPAs that make a large number of network requests to get various pieces of data, it’s essential to optimize how and when these requests are made. Strategies like caching API responses on the client side, batching API calls into one, or using GraphQL to minimize the amount of data transferred all help optimize how API requests are made and how often they are actually made.
How to Improve Perceived Times
When it comes to improving perceived load times, one simply and effective solution is using skeleton screens or skeleton sections, placeholder elements that mimic the layout of a page or section before the actual content has been fetched and loaded into place. Instead of making users stare at a blank screen or a loading spinner, skeletons show that the content is actively loading. It feels more like progress, reducing frustration and hopefully reducing bounce rates. These placeholders typically use gray blocks or blurred images that represent the shape and structure of the page, creating the illusion that things are moving faster.
Lazy loading and content prioritization also play a key role in improving perceived performance. Lazy loading ensures that only the content in view is loaded first, while other elements (like images or videos) load as the user scrolls them into view. Users can then start interacting with the site without waiting for everything to load. Also preloading important resources like fonts or key images helps eliminate delays in allowing the user to interact with the page or app.
Lazy loading images was introduced in 2019 in Google Chrome, and then most other browsers followed with the implementation in 2020. Adding loading="lazy"
is all that is required on images.
Video lazy loading requires a bit of code.
<video width="320" height="240" controls preload="none">
<source data-src="video.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
<script>
document.addEventListener("DOMContentLoaded", function() {
const videos = document.querySelectorAll('video');
const lazyLoadVideo = (video) => {
const source = video.querySelector('source');
source.src = source.dataset.src;
video.load();
};
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
lazyLoadVideo(entry.target);
observer.unobserve(entry.target);
}
});
});
videos.forEach(video => observer.observe(video));
});
</script>
Websites can achieve load times of less than a second and therefore reduce bounce rates by adopting just a few of these techniques. Focusing on minimizing initial load sizes, optimizing assets, and ensuring that server-side rendering and caching strategies are in place is key to optimizing how long a websites takes to load and how long subsequent requests take.