Smaller Documents for Smaller Screens using Sec-CH-Viewport-Width
Tags: webdev, performance, react
Published: 2024-06-27 18:43:54

If you have been following my [engineering blog](https://pillser.com/engineering/), you know that I am obsessive about performance. Pillser lists a lot of data about supplements and research papers, and I want to make sure that the website is fast and responsive. One of the ways I've done it is by using `Sec-CH-Viewport-Width` to determine the width of the viewport and serve smaller documents to mobile devices.

## What is `Sec-CH-Viewport-Width`?

`Sec-CH-Viewport-Width` is a [Client Hints](https://developer.mozilla.org/en-US/docs/Web/HTTP/Client_hints) (CH) header to convey the viewport width of a client's display in CSS pixels. This header allows web servers to adapt their responses based on the actual size of the user's viewport, enabling better optimization of resources like images and layout.

However, by default, the header is not sent by the browser. To enable it, you need to send HTTP response headers with `Accept-CH: Sec-CH-Viewport-Width`. This will [instruct the browser](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-CH) to send the `Sec-CH-Viewport-Width` header in the subsequent requests.

## How does Pillser use `Sec-CH-Viewport-Width`?

If you look at pages like the [supplement search](https://pillser.com/search) or a [specific supplement category page](https://pillser.com/vitamins/vitamin-a), you will notice that (on desktop devices) there is a lot of tabular data being displayed. This data provides valuable information for someone researching supplements, but it is not very readable on mobile devices and it accounts for a lot of the page's weight.

To solve this problem, Pillser uses `Sec-CH-Viewport-Width` to determine the width of the viewport and serve smaller documents to mobile devices. It works just like CSS media queries, but instead of deciding which content to display on a device, it makes the decision on the server. Here is the implementation of `useViewportWidth`:

```ts
import { usePublicAppGlobal } from './usePublicAppGlobal';
import { useEffect, useState } from 'react';

export const useViewportWidth = () => {
  const publicAppGlobal = usePublicAppGlobal();

  const [width, setWidth] = useState<number | null>(
    publicAppGlobal.visitor.viewportWidth,
  );

  useEffect(() => {
    const handleResize = () => {
      setWidth(window.innerWidth);
    };

    window.addEventListener('resize', handleResize);

    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return width;
};
```

On the server, I parse the `Sec-CH-Viewport-Width` header and populate the `visitor.viewportWidth` field in the public app global. This field is then used by the `useViewportWidth` hook to determine the width of the viewport. Here is the server-side logic:

```ts
let viewportWidth: number | null;

try {
  viewportWidth = z
    .number({ coerce: true })
    .min(1)
    .parse(request.headers.get('sec-ch-viewport-width'));
} catch {
  viewportWidth = null;
}
```

And that's really all there is to it. The `Sec-CH-Viewport-Width` header is sent by the browser, Pillser parses it, and uses the result to determine the width of the viewport. This allows Pillser to serve smaller documents to mobile devices, improving the user experience and reducing the page weight.

## Gotchas

Two gotchas to be aware of: browser support and the initial render.

Today, Client Hints are supported by [76% of browsers](https://caniuse.com/?search=client%20hints). The primary browsers that do not support Client Hints are Safari and Firefox. Regarding, Safari iOS, since we are defaulting to the smallest size in absence of the header (see the next section), it is not a problem. As for Safari desktop and Firefox, the website will still work as expected, but it will need to recalculate the content on the client-side. That's a fine trade-off if it means that the majority of visitors will get improved experience.

(You can also add support to Safari and Firefox by implementing pseudo-Client Hints by using cookies to set the viewport width.)

The other gotcha to be aware of is that the browser will only send the `Sec-CH-Viewport-Width` header in subsequent requests, not in the response. This means that the first time a user visits a page, their viewport width will not be known. To fix this, I default to always using the smallest breakpoint when the viewport width is unknown. This way, the mobile devices will still get the correct content, but the desktop UI will be updated upon recalculating the viewport using client-side logic.