Optimizing performance: how our extension became lightning fast
Tags: webdev, frontend, architecture, performance
Published: 2023-10-20 14:41:55

This article is for website creators who have some knowledge of backend and NGINX. It's about how we improved our browser extension to make it faster and better. By reading it, you'll learn about the tools and tricks we use to fix slow loading and other annoying problems.  

## What is Casca?
Casca is a browser [extension](https://casca.space/get/dev) that replaces your default new tab into a personalized dashboard. It lets you set up different workspaces (like one for work and one for personal stuff), customize your dashboard with helpful widgets, interact with ChatGPT, choose nice backgrounds, preview websites and more. It’s all about making your online space tidy and efficient.

## **Why Speed is Important to Us**

At Casca, we prioritize the speed and performance of our extension for several reasons:

1. **Enhanced User Experience**: Our main focus is to make sure our users have a fast and responsive browsing. Applications that are slow can make devices perform poorly and cause browsers to be slow as well. That's why our aim is to give our users a smooth and enjoyable online experience.
2. **Optimized Resource Usage**: We know that many users have devices that don't have a lot of power and that don't last long on a single charge. Apps that take a long time to load can use up these resources and make the device work slower. It is designed to use as little of these resources as possible, so users can browse the internet without their device slowing down or feeling laggy.
3. **Productivity and Efficiency**: A quick app saves time and helps you get things done faster. By reducing the time it takes to load and respond, it allows users to work more efficiently and complete tasks more quickly.
4. **Reliability and Trust**: A slow and unresponsive app can make users lose trust. Our goal is to provide a browsing solution that is dependable and fast. By prioritizing speed and responsiveness, we demonstrate our commitment to delivering a top-notch product that meets user expectations.

## Our tech stack

To build Casca Extension, we used a bunch of different technologies such as:

- **React, Tailwind and React Grid Layout** to make the extension fast, scalable and beautiful
- **IndexedDB** to store large data right in the browser
- **Node.js, Express and MongoDB** to proxy, map and cache external API requests
- **Sharp library** to compress large images
- **NGINX** serves as a load balancer for all requests and utilizes **NGINX Content Caching** to cache slow responses from the **Sharp** **library**

## **How We Fixed Things**

### **Fixing Images & Favicons**

By using **NGINX Content Caching** and **IndexedDB**, we have improved the way we get large background images and multiple favicons. This means we can quickly save and retrieve images from a cache so they can be accessed immediately in the future.

A part of our NGINX configuration:

```
proxy_cache_path  /var/www/cache/nginx levels=1:2 keys_zone=background-images:8m max_size=3000m inactive=1200m;
proxy_temp_path /var/www/cache/nginx/tmp;

location /api/casca/favicon {
  proxy_pass <SERVICE_ADDRESS>/api/casca/favicon;
  proxy_cache my-cache;
  proxy_cache_valid  200 302  24h;
  proxy_cache_valid  404      5m;
}

location /api/optimize {
  proxy_pass <SERVICE_ADDRESS>/api/optimize;
  proxy_cache background-images;
  proxy_cache_valid  200 302  7d;
  proxy_cache_valid  404      1h;
  add_header Access-Control-Allow-Origin *;
  #limit_req zone=one;
}
```

**Express.js middleware:**

```jsx
app.get("/api/optimize", async (req, res) => {
  const image = await fetchImage(req.query.url.toString());
  sharp(image.buffer)
    // webp is a modern image format that provides lossless and lossy compression for images on the web
    .toFormat('webp', { 
    // compress image to 90% of original quality. It doesn't mean that the image will be 10% smaller
      quality: 90,
    })
    // resize image to 1920px width. It's a pretty common resolution for desktops
    .resize({ width: 1920 })
    // make a blob and send it to the client side
    .toBuffer((_, buffer) => res.end(buffer, "binary")); 
})
```

**And usage on the client side:**

```jsx
// Background.jsx
export default function Background({ imageURL, className }) {
  return (
    <img
      src={VITE_RESIZE_ORIGIN + "/api/optimize?url=" + encodeURIComponent(imageURL)}
      className={className}
      alt="Background image"
    />
  )
}
```

These optimizations made a big difference. For example, our background image used to be a large 1.9MB, but with the Sharp library, it's now just 334kB. This not only speeds up loading but also saves on network data and RAM usage. This makes the Casca Extension work smoothly, even on devices with less memory, providing a better user experience.

The original image and our highly optimized copy:

![Image optimization example. From 1.9mb to 334kb](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qhdohqm9nxug5tm0o0ka.png)

The differences in quality are not noticeable to the naked eye.

### **Dealing with slow requests**

To fetch data from other websites, we used **Node.js** and **Express** to create a middleware. It allows to save and serve the data quickly, even if the original website is slow.

In addition, we store the fetched data in the localStorage cache. Therefore, the data is accessed offline and updated only when subsequent requests are fulfilled.

### **Stopping unneeded re-renders**

- **useMemo, useEvent**: We memoized immutable data and functions to avoid redundant re-renders, ensuring that only the components witnessing data alterations are re-rendered.

The implementation of the `useEvent` hook:

```jsx
import { useCallback, useRef } from "react";

export function useEvent(cb) {
  const cbRef = useRef(cb)

  // Update the callback reference when the callback function changes
  cbRef.current = cb

  // Return a memoized callback function. No hook dependencies are needed
  return useCallback((...args) => cbRef.current(...args), [])
}
```

Usage:

```jsx
export default function App() {
  const [value, setValue] = useState(0)

  // 1. It will always return the same function
  // 2. There will be the actual value in the closure
  const handleIncrement = useEvent(() => setValue(value + 1))

  return <SomeLargeComponent onSubmit={handleIncrement} />
}
```

There are multiple names for this hook. You can find the documentation under the names [useEvent](https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md) or [useEffectEvent](https://react.dev/reference/react/experimental_useEffectEvent).

- **keep-unchanged-values**: This library is specifically deployed for regular data fetching scenarios. It prevents unnecessary re-renders by keeping unchanged values and only updating altered data.

```jsx
// useStorageData.js
export function useStorageData(selectorFn) {
  const [data, setData] = useState(() => selectorFn(getData(storageKey)))

  useEffect(() => {
    const unsubscribe = onStorageChange(({ newValue }) => {
      const selectedData = selectorFn(newValue)
      const nextData = keepUnchangedValues(data, selectedData) // <-- the usage is here
      if (nextData !== data) setData(nextData)
    });

    return unsubscribe
  }, [data])

  return data
}

// TodoItems.jsx
export default function TodoItems() {
  const storageData = useStorageData((data) => ({ items: data.todoItems || [] }))

  return (
    <>
      {storageData.items.map(item => <MemoizedTodo key={item.id} item={item} />)}
    </>
  )
}

// Don't need to use deepEquals to check the props equality
// keepUnchangedValues returns an unchanged item
// if it finds it in the previous array (prevProps.data === nextProps.data is true)
const MemoizedTodo = React.memo(function Todo({ data }) {
    return <div>{data.title}</div>
})
```

For more information you can check the library documentation at https://github.com/CascaSpace/keep-unchanged-values

### **Tips for Extension Developers**

To build a fast browser extension, it's important to consider more than just writing efficient code. Here are some tips for frontend developers:

1. **Optimize images and assets:**
Large images can slow down your extension. Compress images and use formats like WebP. Implement lazy loading to load images only when needed.
2. **Speed up server requests:**
Slow server requests can be a performance bottleneck. Use caching, CDNs, and browser caching. Minimize unnecessary API calls by storing data locally.
3. **Avoid unnecessary re-renders:**
Prevent components from re-rendering when data hasn't changed. Use techniques like `useEvent`, `useMemo` and `React.memo` in React to optimize rendering.
4. **Optimize data storage:**
Choose the right data storage method. IndexedDB is great for storing large data sets in the browser. Manage data efficiently and plan for offline access and synchronization.
5. **Focus on user experience:**
Speed should improve the user experience. Test with users and gather feedback to identify areas for improvement. Prioritize a smooth and responsive interface.
6. **Keep an eye on memory usage:**
High memory usage can slow down your extension and the browser. Keep an eye on memory consumption and optimize your code to reduce unnecessary memory use.

By following these tips, you can make sure that your browser extension runs well and efficiently, giving your users a better experience.

### **Wrapping Up**

- Leveraging technologies like React, Tailwind, and React Grid Layout, we have created a visually appealing and scalable extension.
- We have optimized the handling of large images and favicons through NGINX Content Caching and IndexedDB.
- Slow requests have been addressed by implementing Node.js and Express middleware.
- Techniques like `useMemo` and `useEvent` hooks have minimized unnecessary re-renders.
- The integration of the `keep-unchanged-values` library has further improved performance.

We have made the [Casca Extension](https://casca.space/) much faster and more efficient by using different technologies. We fixed problems like slow loading of images and favicons, dealing with slow requests, and reducing unnecessary re-renders. As a result, our users now enjoy a quick and smooth browsing experience. We prioritize speed and responsiveness to ensure a seamless and enjoyable online experience without using too much of your device's resources. We will continue to improve our technology to keep Casca lightweight and efficient for our users.