DEV Community

Cover image for Build an Image Compress Budget Gate Before Heavy Assets Reach Production
王康
王康

Posted on

Build an Image Compress Budget Gate Before Heavy Assets Reach Production

The page looked fine in review. The deployment passed. Then someone opened it on a slower mobile connection and found that the new hero image was nearly 5 MB.

Nothing was technically broken. The file existed, the browser could decode it, and the layout rendered correctly. The problem was that our definition of “ready” did not include an image size limit.

That is where an image budget helps. Instead of treating image compress work as a final manual cleanup, we can make file size part of the build contract. If an asset exceeds the agreed limit, CI should explain the problem before the change reaches production.

This tutorial builds a small, dependency-free image size gate with Node.js and runs it in GitHub Actions.

TL;DR

  • Define limits by asset role, not one universal number.
  • Check individual files and the total image payload.
  • Fail CI with an actionable message.
  • Keep compression separate from validation.
  • Adjust budgets using real production measurements.

Why a valid image can still be a broken artifact

Most build pipelines answer questions such as:

  • Does the file exist?
  • Does the application compile?
  • Do tests pass?
  • Can the browser render the asset?

Those checks are necessary, but they do not tell us whether a file is appropriate for delivery. A 4,000-pixel photograph may be valid while still being wasteful inside a 900-pixel content column.

An image compress workflow reduces the file after someone notices the problem. An image budget moves the decision earlier. It turns “this feels too large” into a rule that can be reviewed, versioned, and enforced.

The goal is not to make every image as small as possible. The goal is to catch accidental weight before it becomes a user-facing regression.

Start with budgets you can explain

A single limit for every image is tempting, but it ignores how assets are used. A full-width hero, a product thumbnail, and a small icon have different requirements.

Here is a reasonable starting point for a content site:

Asset group Example limit
Hero image 350 KB
Article image 220 KB
Thumbnail 100 KB
Icon or badge 30 KB
Total images on one page 1 MB

These are not universal standards. A photography portfolio may accept larger files, while a documentation site can be stricter. What matters is that each limit reflects the rendered size, visual importance, and network conditions of the audience.

Treat the image compress budget as a project constraint rather than a universal score. It should describe what your pages can afford, not force unrelated assets into the same preset.

Start with current production data. Identify the heaviest pages, inspect their image requests, and choose a first budget that catches obvious mistakes without blocking normal work.

Create a dependency-free size checker

The following script scans an image directory, checks every supported file, and fails when either an individual file or the combined folder size exceeds the budget.

Create scripts/check-image-budget.mjs:

import { readdir, stat } from "node:fs/promises";
import { extname, join, relative } from "node:path";

const root = process.argv[2] ?? "public/images";
const maxFileBytes = Number(process.env.MAX_IMAGE_KB ?? 250) * 1024;
const maxTotalBytes = Number(process.env.MAX_IMAGE_TOTAL_KB ?? 1500) * 1024;
const imageExtensions = new Set([
  ".jpg", ".jpeg", ".png", ".webp", ".avif", ".gif"
]);

async function walk(directory) {
  const entries = await readdir(directory, { withFileTypes: true });
  const files = [];

  for (const entry of entries) {
    const fullPath = join(directory, entry.name);

    if (entry.isDirectory()) {
      files.push(...await walk(fullPath));
    } else if (imageExtensions.has(extname(entry.name).toLowerCase())) {
      files.push(fullPath);
    }
  }

  return files;
}

function toKB(bytes) {
  return `${(bytes / 1024).toFixed(1)} KB`;
}

const files = await walk(root);
const failures = [];
let totalBytes = 0;

for (const file of files) {
  const { size } = await stat(file);
  totalBytes += size;

  if (size > maxFileBytes) {
    failures.push(
      `${relative(root, file)} is ${toKB(size)}; limit is ${toKB(maxFileBytes)}`
    );
  }
}

if (totalBytes > maxTotalBytes) {
  failures.push(
    `Total image size is ${toKB(totalBytes)}; limit is ${toKB(maxTotalBytes)}`
  );
}

if (failures.length > 0) {
  console.error("Image budget exceeded:\n");
  for (const failure of failures) console.error(`- ${failure}`);
  console.error("\nResize, convert, or compress the affected assets, then run the check again.");
  process.exit(1);
}

console.log(
  `Image budget passed: ${files.length} files, ${toKB(totalBytes)} total.`
);
Enter fullscreen mode Exit fullscreen mode

Run it locally with:

MAX_IMAGE_KB=250 \
MAX_IMAGE_TOTAL_KB=1500 \
node scripts/check-image-budget.mjs public/images
Enter fullscreen mode Exit fullscreen mode

This first version intentionally checks bytes, not perceived quality. CI can tell us that a file is too large; a person still needs to decide whether resizing, format conversion, or a gentler image compress setting is the correct fix.

Add the gate to GitHub Actions

Run the same command on pull requests so the feedback appears before merge:

name: Image budget

on:
  pull_request:
    paths:
      - "public/images/**"
      - "scripts/check-image-budget.mjs"
      - ".github/workflows/image-budget.yml"

jobs:
  check-images:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - name: Check image budget
        env:
          MAX_IMAGE_KB: 250
          MAX_IMAGE_TOTAL_KB: 1500
        run: node scripts/check-image-budget.mjs public/images
Enter fullscreen mode Exit fullscreen mode

The paths filter avoids running the job when a pull request changes unrelated code. The limits remain visible in the workflow, which makes a budget change an explicit code review decision.

Make failures actionable

A gate is useful only when developers know how to recover. “Build failed” is not enough. The check should name the file, show its current size, state the allowed size, and suggest the next step.

A useful image compress gate therefore reports a repair path, not only a red status icon.

When a file fails, use this order:

  1. Check dimensions. If the source is much wider than its largest rendered size, resize it first.
  2. Check the format. A photograph exported as PNG may be better suited to JPEG, WebP, or AVIF.
  3. Apply image compression. Compare at least two quality levels at the actual display size.
  4. Retest the page. Confirm that text, gradients, transparency, and important details still look correct.
  5. Run the budget check again. Validation should use the final file that will be deployed.

For occasional assets, a browser tool such as ImageCompress can help compare a smaller output without opening a full image editor. For private or unreleased material, review the service’s file-handling policy or keep processing inside an approved local workflow.

Do not let CI silently rewrite assets

It may seem convenient to have CI automatically run an image compress command and commit the result. In practice, that hides an important visual decision.

Different assets fail in different ways. Photographs can develop blocking around edges, screenshots can lose text clarity, and transparent graphics can gain halos. A fully automatic rewrite may pass the byte limit while damaging the image’s purpose.

A safer separation is:

  • local tooling or a controlled build step creates optimized candidates;
  • a developer reviews the visual result;
  • CI validates the committed output against the budget.

Automation should prevent regressions, not remove the quality review.

Improve the rule as the project grows

The basic script is intentionally small. A production system can extend it in several ways:

  • use different limits for hero/, content/, and thumbnails/;
  • inspect pixel dimensions with an image metadata library;
  • compare only files changed in the pull request;
  • publish a size report as a pull request comment;
  • track page-level transfer size with Lighthouse or a browser test;
  • allow documented exceptions for assets that genuinely need more detail.

Be careful with permanent exceptions. If every failure becomes a larger limit, the budget stops protecting anything. An exception should explain why the heavier asset is necessary and which page owns the cost.

What this check does not solve

A small file can still load late. The browser may discover it only after JavaScript runs, the server may omit effective caching, or the page may lazy-load its main visual by mistake.

The budget covers transfer weight. It does not replace responsive srcset candidates, correct width and height, caching, request prioritization, or performance measurement in real browsers.

Think of it as one reliable boundary in a larger delivery system.

Pre-merge checklist

  • [ ] The source dimensions match the asset’s intended display size.
  • [ ] The format fits the visual content.
  • [ ] The image compress result was reviewed at normal zoom.
  • [ ] The file passes its individual size budget.
  • [ ] The page stays within its total image budget.
  • [ ] Responsive variants exist where they provide a real benefit.
  • [ ] The pull request explains any approved exception.
  • [ ] The deployed request size is verified in browser tools.

Final thoughts

The most expensive image regressions are often ordinary mistakes: an original camera export, a screenshot saved in the wrong format, or a replacement asset with much larger dimensions than the file before it.

A small CI gate catches those mistakes while they are still easy to fix. It also turns image compress decisions into shared engineering rules instead of relying on someone to remember a checklist before every release.

Start with one directory and one understandable limit. Run the check in warning mode if necessary, observe what it catches, and tighten the budget using real page data. Once the rule reflects the project’s needs, make it a required check.

The build should not only prove that an image can ship. It should also prove that the image belongs in the delivery budget.

Top comments (0)