Caching
What is caching?
Caching is a technique used to store copies of files or data in a temporary storage location, known as a cache, to reduce the time it takes to access them. The primary goal of caching is to improve performance by minimizing the need to fetch data from the original source repeatedly.
Types of caching
Browser cache
The browser cache stores copies of web pages, images, and other resources locally on the user’s device. When a user revisits a website, the browser can load these resources from the cache instead of fetching them from the server, resulting in faster load times.
Service workers
Service workers are scripts that run in the background and can intercept network requests. They can cache resources and serve them from the cache, even when the user is offline. This can significantly improve performance and provide a better user experience.
HTTP caching
HTTP caching involves using HTTP headers to control how and when resources are
cached. Common headers include Cache-Control, Expires, and ETag.
How caching improves performance
Reduced latency
By storing frequently accessed data closer to the user, caching reduces the time it takes to retrieve that data. This results in faster load times and a smoother user experience.
Reduced server load
Caching reduces the number of requests made to the server, which can help decrease server load and improve overall performance.
Offline access
With service workers, cached resources can be served even when the user is offline, providing a seamless experience.
Implementing caching
Browser cache example
<head>
<link rel="stylesheet" href="styles.css" />
<script src="app.js"></script>
</head>Service worker example
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open("v1").then((cache) => {
return cache.addAll(["/index.html", "/styles.css", "/app.js"]);
})
);
});
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});HTTP caching example
Cache-Control: max-age=3600Debouncing and throttling
Debouncing
Debouncing is a technique used to ensure that a function is only executed after a certain amount of time has passed since it was last invoked. This is particularly useful in scenarios where you want to limit the number of times a function is called, such as when handling user input events like keypresses or mouse movements.
Example
Imagine you have a search input field and you want to make an API call to fetch search results. Without debouncing, an API call would be made every time the user types a character, which could lead to a large number of unnecessary calls. Debouncing ensures that the API call is only made after the user has stopped typing for a specified amount of time.
function debounce(func, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
// Usage
const handleSearch = debounce((query) => {
// Make API call
console.log("API call with query:", query);
}, 300);
document.getElementById("searchInput").addEventListener("input", (event) => {
handleSearch(event.target.value);
});Throttling
Throttling is a technique used to ensure that a function is called at most once in a specified time interval. This is useful in scenarios where you want to limit the number of times a function is called, such as when handling events like window resizing or scrolling.
Example
Imagine you have a function that updates the position of elements on the screen based on the window size. Without throttling, this function could be called many times per second as the user resizes the window, leading to performance issues. Throttling ensures that the function is only called at most once in a specified time interval.
function throttle(func, limit) {
let inThrottle;
return function (...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
// Usage
const handleResize = throttle(() => {
// Update element positions
console.log("Window resized");
}, 100);
window.addEventListener("resize", handleResize);Lazy loading
What is lazy loading?
Lazy loading is a design pattern used to defer the initialization of an object until the point at which it is needed. This can be applied to various types of resources such as images, videos, scripts, and even data fetched from APIs.
How does lazy loading work?
Lazy loading works by delaying the loading of resources until they are actually
needed. For example, images on a webpage can be lazy-loaded so that they only
load when they come into the viewport. This can be achieved using the
loading="lazy" attribute in HTML or by using JavaScript libraries.
Benefits of lazy loading
- Improved performance: By loading only the necessary resources initially, the page load time is reduced, leading to a faster and more responsive user experience.
- Reduced bandwidth usage: Lazy loading helps in conserving bandwidth by loading resources only when they are needed.
- Better user experience: Users can start interacting with the content faster as the initial load time is reduced.
Implementing lazy loading
Using the loading attribute in HTML
The simplest way to implement lazy loading for images is by using the loading
attribute in HTML.
<img src="image.jpg" loading="lazy" alt="Lazy loaded image" />Using JavaScript
For more complex scenarios, you can use JavaScript to implement lazy loading. Here is an example using the Intersection Observer API:
Example
document.addEventListener("DOMContentLoaded", function () {
const lazyImages = document.querySelectorAll("img.lazy");
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove("lazy");
observer.unobserve(img);
}
});
});
lazyImages.forEach((image) => {
imageObserver.observe(image);
});
});In this example, images with the class lazy will only load when they come into
the viewport.
Optimizing DOM manipulation for better performance
Minimize direct DOM access
Accessing the DOM is relatively slow, so try to minimize the number of times you read from or write to the DOM. Instead, store references to elements in variables and work with those.
const element = document.getElementById("myElement");
element.style.color = "red";
element.style.backgroundColor = "blue";Batch DOM changes
Instead of making multiple changes to the DOM one at a time, batch them together. This reduces the number of reflows and repaints.
const element = document.getElementById("myElement");
element.style.cssText = "color: red; background-color: blue;";Use documentFragment
When adding multiple elements to the DOM, use a documentFragment to minimize
reflows and repaints.
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const newElement = document.createElement("div");
newElement.textContent = `Item ${i}`;
fragment.appendChild(newElement);
}
document.getElementById("container").appendChild(fragment);Leverage virtual DOM libraries
Libraries like React use a virtual DOM to batch updates and minimize direct DOM manipulation, which can significantly improve performance.
import React from "react";
import ReactDOM from "react-dom";
const App = () => (
<div>
<h1>Hello, world!</h1>
</div>
);
ReactDOM.render(<App />, document.getElementById("root"));Use requestAnimationFrame for animations
For smoother animations, use requestAnimationFrame to ensure updates are
synchronized with the browser’s repaint cycle.
function animate() {
// Update animation state
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);Avoid layout thrashing
Layout thrashing occurs when you read and write to the DOM repeatedly in a way that forces the browser to recalculate styles and layout multiple times. To avoid this, separate read and write operations.
const element = document.getElementById("myElement");
const height = element.clientHeight; // Read
element.style.height = `${height + 10}px`; // WriteOptimizing network requests for better performance
Minimize the number of requests
Reducing the number of network requests can significantly improve performance. Here are some strategies:
- Combine files: Merge multiple CSS or JavaScript files into a single file.
- Image sprites: Combine multiple images into a single sprite sheet and use CSS to display the correct part of the image.
- Inline small assets: Use data URIs to inline small images or fonts directly into your CSS or HTML.
Use caching
Caching can reduce the need to make network requests for resources that have not changed:
- HTTP caching: Use
Cache-ControlandETagheaders to control how and when resources are cached by the browser. - Service workers: Implement service workers to cache assets and API responses, allowing your app to work offline and load faster on subsequent visits.
Compress data
Reducing the size of the data being transferred can speed up network requests:
- Gzip/Brotli compression: Enable Gzip or Brotli compression on your server to compress HTML, CSS, and JavaScript files.
- Minification: Minify CSS, JavaScript, and HTML files to remove unnecessary characters and reduce file size.
Leverage modern web technologies
Modern web technologies can help optimize network performance:
- HTTP/2: Use HTTP/2 to take advantage of multiplexing, header compression, and server push, which can reduce latency and improve load times.
- CDNs: Use Content Delivery Networks (CDNs) to serve static assets from locations closer to the user, reducing latency.
Optimize images
Images often account for a large portion of the data transferred:
- Responsive images: Use the
srcsetattribute to serve different image sizes based on the user’s device. - Image formats: Use modern image formats like WebP or AVIF, which offer better compression than traditional formats like JPEG or PNG.
- Lazy loading: Implement lazy loading to defer loading images until they are needed.
Reduce payload size
Reducing the amount of data sent in each request can improve performance:
- API optimization: Optimize API responses to include only the necessary data.
- GraphQL: Use GraphQL to request only the specific data needed by the client.
Common performance bottlenecks in JavaScript applications
Common performance bottlenecks in JavaScript applications include inefficient DOM manipulation, excessive use of global variables, blocking the main thread with heavy computations, memory leaks, and improper use of asynchronous operations. To mitigate these issues, you can use techniques like debouncing and throttling, optimizing DOM updates, and leveraging web workers for heavy computations.
Inefficient DOM manipulation
Frequent DOM updates
Frequent DOM updates can be costly because the browser has to re-render the page each time the DOM changes. Batch DOM updates together to minimize reflows and repaints.
Example
// Inefficient
for (let i = 0; i < 1000; i++) {
const div = document.createElement("div");
div.textContent = i;
document.body.appendChild(div);
}
// Efficient
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const div = document.createElement("div");
div.textContent = i;
fragment.appendChild(div);
}
document.body.appendChild(fragment);Layout thrashing
Layout thrashing occurs when you read and write to the DOM repeatedly, causing multiple reflows and repaints. Minimize layout thrashing by batching reads and writes separately.
Example
// Inefficient
for (let i = 0; i < 1000; i++) {
const height = element.clientHeight;
element.style.height = `${height + 10}px`;
}
// Efficient
const height = element.clientHeight;
for (let i = 0; i < 1000; i++) {
element.style.height = `${height + 10}px`;
}Excessive use of global variables
Global variables can lead to memory leaks and make the code harder to maintain. Use local variables and closures to limit the scope of variables.
// Inefficient
var globalVar = "I am global";
// Efficient
function myFunction() {
let localVar = "I am local";
}Blocking the main thread
Heavy computations
Heavy computations can block the main thread, making the UI unresponsive. Use web workers to offload heavy computations to a background thread.
// Main thread
const worker = new Worker("worker.js");
worker.postMessage("start");
// worker.js
self.onmessage = function (e) {
if (e.data === "start") {
// Perform heavy computation
self.postMessage("done");
}
};Synchronous operations
Avoid synchronous operations like alert, prompt, and synchronous XHR
requests, as they block the main thread.
// Inefficient
alert("This blocks the main thread");
// Efficient
console.log("This does not block the main thread");Memory leaks
Memory leaks occur when memory that is no longer needed is not released. Common causes include circular references and unremoved event listeners.
Circular references
// Inefficient
function createCircularReference() {
const obj1 = {};
const obj2 = {};
obj1.ref = obj2;
obj2.ref = obj1;
}
// Efficient
function createNonCircularReference() {
const obj1 = {};
const obj2 = {};
obj1.ref = obj2;
}Unremoved event listeners
// Inefficient
element.addEventListener("click", handleClick);
// Efficient
element.removeEventListener("click", handleClick);Improper use of asynchronous operations
Unoptimized promises
Chain promises properly to avoid blocking the main thread.
// Inefficient
fetch("url")
.then((response) => response.json())
.then((data) => {
// Process data
});
// Efficient
async function fetchData() {
const response = await fetch("url");
const data = await response.json();
// Process data
}
fetchData();Debouncing and throttling
Use debouncing and throttling to limit the rate of function execution, especially for event handlers.
// Debouncing
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
// Throttling
function throttle(func, limit) {
let inThrottle;
return function (...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}Techniques for reducing reflows and repaints
Minimize DOM manipulations
Frequent changes to the DOM can cause multiple reflows and repaints. To minimize this:
- Use
documentFragmentto batch DOM updates - Clone nodes, make changes, and then replace the original node
Batch DOM changes
Grouping multiple DOM changes together can reduce the number of reflows and repaints:
- Use
innerHTMLto update multiple elements at once - Use
requestAnimationFrameto batch updates
Use CSS classes for style changes
Instead of changing styles directly via JavaScript, use CSS classes:
element.classList.add("new-class");Avoid complex CSS selectors
Complex selectors can slow down the rendering process:
- Use simple and direct selectors
- Avoid deep nesting
Use requestAnimationFrame for animations
Using requestAnimationFrame ensures that animations are synchronized with the
browser’s repaint cycle:
function animate() {
// Animation logic
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);Use will-change for frequently changing elements
The will-change property can hint to the browser about which elements will
change, allowing it to optimize rendering:
.element {
will-change: transform;
}Avoid layout thrashing
Reading and writing to the DOM separately can prevent layout thrashing:
const height = element.offsetHeight; // Read
element.style.height = `${height + 10}px`; // WriteTools to measure and analyze JavaScript performance
Chrome DevTools
Chrome DevTools is a set of web developer tools built directly into the Google Chrome browser. It provides a Performance panel that allows you to record and analyze the runtime performance of your web application.
- Open Chrome DevTools by pressing
F12orCtrl+Shift+I - Navigate to the Performance panel
- Click the “Record” button to start profiling
- Interact with your web application to capture performance data
- Click the “Stop” button to end profiling and analyze the results
Lighthouse
Lighthouse is an open-source, automated tool for improving the quality of web pages. It provides audits for performance, accessibility, progressive web apps, SEO, and more.
- Open Chrome DevTools
- Navigate to the Lighthouse panel
- Select the categories you want to audit (e.g., Performance)
- Click the “Generate report” button
- Review the generated report for performance insights and recommendations
WebPageTest
WebPageTest is a free online tool that provides detailed performance testing of web pages. It allows you to run tests from multiple locations around the world using real browsers.
- Go to WebPageTest
- Enter the URL of the web page you want to test
- Select the test location and browser
- Click the “Start Test” button
- Review the detailed performance report, including metrics like load time, time to first byte, and more
JSPerf
JSPerf is a tool for comparing the performance of different JavaScript snippets. It allows you to create and run benchmarks to see which code performs better.
- Go to JSPerf
- Create a new test case by entering different JavaScript snippets
- Run the benchmark to compare the performance of the snippets
- Review the results to see which snippet performs better