If you have been following my engineering blog, 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 (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 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 or a specific supplement category page, 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
:
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:
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. 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.
Top comments (0)