Select theme appearance

Website Performance Improvements

A collection of ways to increase your website's performance.

  • Tags:  Web Development, Web Performance
  • Last updated:  

INP and improving responiveness of user interactions

Optimising long(ish) tasks through breaking up work

When you have user interaction, that require instant feedback and also take a lot of time, things can get tricky. There has been recent research on how to improve it this one article by kurtextrem did a great job at summarising it.

This is mostly a collection for me, so even more condensed:

interactionResponse

Instructs the browser to await the next paint cycle before executing the next step. This prevents unnecessary layout recalculations and layout thrashing, that might occur during the execution of the task.

When running long tasks and trying to keep the UI responsive, it’s good idea to call it at the start of your long task.

export const interactionResponse = () => {
    return new Promise((resolve) => {
        setTimeout(resolve, 100); // Fallback for the case where the animation frame never fires.
        requestAnimationFrame(() => {
            setTimeout(resolve);
        });
    });
};

yieldToMain

Used to break up longer work into smaller tasks. This prevents completely blocking the main thread and thus can increase the responsiveness of the UI.

A good usecase is when dynamically updating filters or search results, that might take a while. In general useful, when iterating through lots of items.

declare global {
    interface Window {
        scheduler: {
            yield?: (options?: YieldOptions) => Promise<void>;
            postTask?: (fn: () => void, options?: YieldOptions) => void;
        };
    }
}

type YieldOptions = {
    signal?: AbortSignal;
    priority?: 'user-blocking' | 'user-visible' | 'background';
};

/**
 * Yields to main thread before continuing execution.
 * If priority is 'user-blocking', it will asynchronously resolve in older browsers.
 * @param {object} options - see [https://github.com/WICG/scheduling-apis/blob/main/explainers/yield-and-continuation.md](spec)
 * @see https://kurtextrem.de/posts/improve-inp for reference
 */
export const yieldToMain = (options?: YieldOptions) => {
    if ('scheduler' in window) {
        if ('yield' in window.scheduler) {
            return window.scheduler.yield?.(options);
        }

        if ('postTask' in window.scheduler) {
            return window.scheduler.postTask?.(() => {}, options);
        }
    }

    // `setTimeout` could suffer from being delayed for longer - so for browsers not supporting yield,
    // we guarantee execution for high priority actions, but it doesn't yield as trade-off.
    if (options?.priority === 'user-blocking') {
        return Promise.resolve();
    }
    return new Promise((resolve) => setTimeout(resolve));
};

// A little helper which yields before running the function
export const yieldBefore = async (fn: () => void, options: YieldOptions) => {
    await yieldToMain(options);
    return fn();
};

For an example of how I used it, read the project description of vegan-hamburg. Be sure to check out the articles below to learn more.

Sources and more information

content-visiblity

An almost magical CSS property, that can drastically help you improve your website’s initial loading times, especially when you have a lot of content. It achieves this by skipping the rendering of elements that are not visible on the screen.

content-visibility: auto;
content-intrinsic-width: auto;
content-intrinsic-height: auto 770px;

Read about it: https://developer.mozilla.org/en-US/docs/Web/CSS/content-visibility

Create a SVG Sprite Map instead of inlining SVGs, when using them multiple times

Inlining SVGs can blow up your DOM size a lot. Each SVG has to be parsed individually and takes quite a bit. Instead, you can “easily” create a sprite map, that contains all your SVGs and then reference them by their ID. This still keeps on demand styling possible.

Example Sprite Map

A file named icons.svg:

<svg xmlns="http://www.w3.org/2000/svg">
		<symbol id="img-chevron-right" viewBox="0 -960 960 960">
			<path d="M504-480 320-664l56-56 240 240-240 240-56-56 184-184Z"/>
		</symbol>
		<symbol id="img-location" viewBox="0 -960 960 960">
			<path xmlns="http://www.w3.org/2000/svg" d="M480-301q99-80 149.5-154T680-594q0-56-20.5-95.5t-50.5-64Q579-778 544-789t-64-11q-29 0-64 11t-65 35.5q-30 24.5-50.5 64T280-594q0 65 50.5 139T480-301Zm0 101Q339-304 269.5-402T200-594q0-71 25.5-124.5T291-808q40-36 90-54t99-18q49 0 99 18t90 54q40 36 65.5 89.5T760-594q0 94-69.5 192T480-200Zm0-320q33 0 56.5-23.5T560-600q0-33-23.5-56.5T480-680q-33 0-56.5 23.5T400-600q0 33 23.5 56.5T480-520ZM200-80v-80h560v80H200Zm280-514Z"/>
		</symbol>
</svg>

Create elements for each SVG you want to use. They need to have a viewBox and an ID to be referenced later.

In your code, reference them like this:

<svg
     class="nc-icon n-opener-icon"
     aria-hidden="true"
     data-size="inline"
     >
     <use href="/icons.svg#img-chevron-right" />
</svg>

And that’s it. You can use them just like inline SVGs, but they are only loaded once and hte sprite sheet can even be cached.

Using faster CSS selectors

When you’re working with a lot of elements, it’s important to use the most efficient CSS selectors. Oftentimes, this is not really required, but it can make noticable difference. It’s also just a good practice and learning for me, which I will adopt and use from now on - it doesn’t hurt.

Since CSS is parsed from right to left, using general selectors for nested elements (like .myList ul li a) can take a while, since it will check every <a> first.

So what to do?

  • Use more classes and more specific selectors
  • Avoid using * and ~ selectors when possible
  • Prefer classes over combined selectors, if possible.

Funnily enough, patterns like BEM are already doing a good job at this.

Beware: It’s still not a good practice to style all elements individually. Prefer composition and building on larger pattern, but target your exceptions specifically.

For this, check