Tag: frontend

  • How to optimize images for the web

    How to optimize images for the web

    If a page feels slow, images are the first thing I check, and they are almost always the culprit. Text and code are tiny. A single uncompressed hero photo can outweigh your entire JavaScript bundle. The good news is that image optimization is mostly mechanical, and you can automate the whole thing.

    Choose the right format

    Format choice is the highest-leverage decision you make. JPEG is fine for photographs but it is old and inefficient. PNG is for images that need transparency or sharp edges, like logos and screenshots, and it is terrible for photos. The modern answer for almost everything is WebP, which gives you JPEG-quality photos at a fraction of the size, and AVIF when you want to push compression even further. I serve AVIF with a WebP fallback and a JPEG fallback below that.

    • Photographs: AVIF or WebP, never raw JPEG if you can avoid it.
    • Logos and icons: SVG when possible, it scales infinitely and weighs nothing.
    • Screenshots with text: PNG or WebP lossless so the text stays crisp.

    Resize before you compress

    This is the mistake I see constantly. People take a 4000 pixel wide camera photo, compress it, and display it in a 600 pixel column. The browser downloads all those wasted pixels and throws them away. Resize the image to the largest size it will actually be shown at first, then compress. Resizing alone often cuts file size by 80 percent before you have touched quality settings.

    To know the right target width, look at how wide the image actually renders in your layout, then double it for high-density screens. A photo shown at 600 pixels should be exported at 1200 so it stays crisp on a retina display, and no larger. Going beyond that is pure waste, because the extra pixels never reach a single eye. I keep a small table of the render widths in my design and resize to match each one.

    Automate it with sharp

    I do not hand-edit images in an app. I run them through a script using the sharp library, which is fast and produces excellent output. A short pipeline resizes and converts everything in a folder:

    const sharp = require('sharp');
    
    sharp('input.jpg')
      .resize({ width: 1200, withoutEnlargement: true })
      .webp({ quality: 75 })
      .toFile('output.webp')
      .then(() => console.log('done'));

    Quality 75 on WebP is my default. The difference between 75 and 90 is invisible on a screen but doubles the file size. Drop it into your build and every image gets the same treatment automatically, which fits neatly into a pipeline run through CI/CD with GitHub Actions.

    Serve responsive sizes

    A phone and a desktop should not download the same image. Generate a few widths and let the browser pick using srcset. The markup tells the browser what sizes exist and how wide the image renders, and it grabs the smallest one that still looks sharp:

    <img
      src="photo-800.webp"
      srcset="photo-400.webp 400w, photo-800.webp 800w, photo-1200.webp 1200w"
      sizes="(max-width: 600px) 100vw, 600px"
      alt="A descriptive caption"
    >

    Lazy load everything below the fold

    Add loading="lazy" to images that are not visible when the page first paints. The browser then defers loading them until the user scrolls near. This is a one-attribute change with a big payoff, because it stops offscreen images from competing for bandwidth with the content people actually see. Leave it off your hero image though, since you want that one to load immediately.

    Always set width and height

    Set explicit width and height attributes, or a CSS aspect ratio, on every image. Without them the browser does not know how much space to reserve, so the page jumps around as images load. That jump is layout shift, and it is one of the metrics Google grades you on. It also just feels broken to users. Reserving the space costs nothing and fixes it completely.

    The payoff

    Put these together and a typical image-heavy page goes from several megabytes to a few hundred kilobytes with no visible quality loss. That shows up directly in your load times and your Core Web Vitals. Lighter images mean a snappier site everywhere, including the moment you push it live through Cloudflare Pages and your visitors load it from the edge. Optimize once in the build, and you never have to think about it per-image again.

  • How to add full-text search to a static site

    How to add full-text search to a static site

    The first objection I hear is that static sites cannot have search because there is no server to query. That is wrong. You can build a search index when the site is generated and ship it as a plain file, then search it entirely in the browser. For anything up to a few thousand documents, it is fast enough that users assume there is a backend.

    Why client-side search works

    The trick is moving the work to build time. While my generator is already looping over every page to render HTML, it is trivial to also collect the title, URL, and body text of each one into a list. That list becomes a JSON file. The browser downloads it once, builds an in-memory index, and every keystroke after that is instant because nothing leaves the device. No database, no API, no per-query cost.

    Build the index at generation time

    During the build, I strip HTML tags from each page and push a small record into an array. Keep the body text trimmed; you do not need every word, and a leaner index downloads faster. Here is the gist of it:

    const index = pages.map(page => ({
      title: page.title,
      url: page.url,
      excerpt: page.excerpt,
      body: page.text.slice(0, 2000)
    }));
    
    fs.writeFileSync('dist/search-index.json', JSON.stringify(index));

    Writing this file is just one more step in the same pipeline that produces your pages, so it slots naturally into a build you may already run through CI/CD with GitHub Actions. The index ships alongside your HTML to the same edge network.

    Pick a search library, or write the dumb version

    For small sites you genuinely can write your own matcher in a dozen lines. Lowercase everything, split the query into words, and score documents by how many words they contain. It works. But the moment you want typo tolerance, prefix matching, or relevance ranking, reach for a library. I like Fuse.js for fuzzy matching and MiniSearch when I want proper full-text scoring. Both are tiny and run in the browser without a build step.

    My rule of thumb is to start with the hand-written version and only upgrade when users complain. Most of the time the simple matcher is more than enough, and you avoid shipping a dependency you do not need. When you do switch to a library, the index format stays the same, so the migration is a few lines, not a rewrite.

    import MiniSearch from 'minisearch';
    
    const res = await fetch('/search-index.json');
    const docs = await res.json();
    
    const mini = new MiniSearch({
      fields: ['title', 'body'],
      storeFields: ['title', 'url', 'excerpt']
    });
    mini.addAll(docs);
    
    const results = mini.search('cloudflare deploy', { fuzzy: 0.2 });

    Wire it to the input

    Hook a listener to your search box, but do not search on every single keystroke. Debounce it by 150 milliseconds or so, otherwise a fast typist fires a dozen searches for one word. On each debounced event, run the query, take the top handful of results, and render them as a list of links. Show the title and the excerpt so people can tell which result they want before clicking.

    • Debounce input so you search on a pause, not on every letter.
    • Limit to the top eight or ten results; nobody scrolls a search dropdown.
    • Highlight the matched term in the result so the relevance is obvious.
    • Handle the empty state and the no-results state explicitly.

    Load the index lazily

    Do not download the search index on page load. Most visitors never search, so paying that cost upfront is wasteful. I fetch the JSON the first time someone focuses the search box, cache it in a variable, and reuse it. The first search has a tiny delay while the file arrives, every search after is instant, and people who never search never pay a byte for it. This keeps your initial load lean, which matters for the same reasons I obsess over in optimizing images for the web.

    When to stop and use a service

    Client-side search has a ceiling. Past roughly ten thousand documents the index gets large enough that downloading and parsing it hurts, especially on phones. At that scale I switch to a hosted search service that exposes an API. But honestly, most blogs and docs sites never come close to that limit. Build the index, ship the JSON, search in the browser, and you get a feature that feels expensive for almost no cost and zero servers to maintain.