• Core Web Vital
  • React Router
  • React
  • Remix

Improve Core Web Vital report by optimizing image loading

Lazy Panda

Improve Core Web Vital report by optimizing image loading

improve-image-loading-core-web-vital

When we are deploying the website, our first concern to load the web page as fast as possible, then we also think what could be the page performance across different device and networks. Core Web Vital would a best way to measure the application page speed report. Light house or PageSpeed Insight could be a great tool to measure the application performance report.

To make this guide easier to discover and more relevant for readers, it now naturally covers Core Web Vital, React Router, React, and Remix.

Images are responsible for the Largest Contentful Paint on 85% of desktop pages and 76% of mobile pages, according to the 2025 Web Almanac. That makes image optimization the single most impactful thing you can do for your Core Web Vitals. But images do not just affect loading speed. They can cause layout shifts, and on image-heavy pages, even slow down interactivity.

A basic image handling (native javaScript)

This is the simplest approach to optimized image loading in React Router or Remix. It's just good ol' HTMLimg component using srcset and sizes attributes, along with a blurred placeholder image set using the CSS background property. The only Remix-y thing we do is exporting a links function to preload the placeholder image.

export default function Image() {
  return (
    <div
      className="max-w-[500px] aspect-[1/1]"
      style={{
        background: `top / contain no-repeat url(${placeholderImgSrc})`,
      }}
    >
      {" "}
      <img
        className="w-full"
        alt="background image"
        src={fallbackImgSrc}
        sizes="(min-width: 400px) 800px, 100vw"
        srcSet={`${smallImgSrc} 400w, ${mediumImgSrc} 640w, ${largeImgSrc} 800w`}
      />{" "}
    </div>
  );
}

The most acceptable and well performed image format would be .webp and .avif.

Generate webp format image

We use ffmpeg to generate these images from an original photo. The command below scales the image to a specific width (in pixels) and sets the image quality (-q:v) to 80 (out of 100) to reduce the file size:

$ ffmpeg -i ./bg.jpg -vf scale=200:-1 -q:v 80 ./bg.webp

The placeholder image is created by scaling down the original image and applying a box blur effect:

$ ffmpeg -i ./bg.jpg -vf "scale=iw/10:-1,boxblur=5:1" ./bg_placeholder.webp

In React Router or Remix world, to ensure the placeholder image loads as quickly as possible, we preload it using the links export for the route:

export function links() {
  return [{ rel: "preload", href: placeholderImgSrc, as: "image" }];
}

Optimized images using vite-plugin-image-optimizer and image-tools

If you have a bunch of local images and you want to compress or optimized those images during build time, then vite-plugin-image-optimizer could be a good options to try out. vite-image-optimizer is nice for passing over all images and optimizing them at build.

Some downsides is that it doesn't convert from one format to another, and it applies the same optimization to all images. If you're wanting to use srcset and sizes to ship different images to different devices, you'll need to use a different tool.

You need to install both the following packages -

npm install vite-plugin-image-optimizer --save-dev npm install vite-imagetools --save-dev

And the following is the vite.config.ts file structure.

import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import rehypePrettyCode from "rehype-pretty-code";
import { ViteImageOptimizer } from "vite-plugin-image-optimizer";
import { imagetools } from "vite-imagetools";
export default defineConfig({
  resolve: { tsconfigPaths: true },
  plugins: [
    ViteImageOptimizer({
      logStats: true,
      test: /bg\.*/,
      jpg: { quality: 80 },
      cache: true,
      cacheLocation: "node_modules/.cache",
    }),
    imagetools(),
    reactRouter(),
  ],
});

You can measure the performace now, I am pettry sure you would see the before and after difference.

Happy Coding!

LazyPanda

Sync up on the latest from LazyPanda.

The truth is, most of us DISCOVER where we are heading when we ARRIVE.