author Avatar

hero Image Blog

8 min read

From Slow to Snappy: The Story Behind Building Image Composer

Thought-Process WebDevelopement

Published by

Author avatar

Abdul Rafay


Share this Blog Post:


AI Summary

As you may know, my blog has evolved significantly over time, transforming from a simple online journal to a comprehensive reflection of my personality and interests. This digital space has become a sanctuary where I can freely express my thoughts and feelings without reservation. However, building this online home, brick by brick, has not been without its challenges. When I was working on version 5 of my application, I encountered an unexpected and unprecedented problem - one that I never thought I’d face, but here we are.

The Web Flow

Before I dive into the problem I’m facing, let me give you a quick rundown of how my website works. It’s actually pretty simple. My site is built using Astro, React, Markdown, and Tailwind CSS. Each of these technologies plays a crucial role: Astro handles content rendering, React takes care of the UI, Tailwind CSS adds styles, and I write all my content in Markdown files.

I use a CMS to create content, which is then pushed to a GitHub repo called Astro-Blog. A GitHub action takes care of building and deploying the site to the internet. It’s a simple, clean setup with no backend – just code and a lot of Markdown files. Every time I add a new file or image to the repo, a new build is generated. This whole process is called Static Site Generation.

The Problem

Now that you know how my site works, let’s talk about the issue I’m facing. It’s a pretty straightforward problem, but it’s been causing me some headaches. I’m talking about asset management. I add images to my blog posts, and there are a lot of them. Depending on the project, the number of images can vary. The problem is that these images are hosted in the _public folder, which needs to be small and fast. As I add more elements to the images, their size increases, causing performance issues.

You might be thinking, “Why are you adding so many large images to the public folder? You shouldn’t be doing that!” And you’re right; I shouldn’t be. But here’s the thing: my blog thumbnails have changed, and I need to move forward, not backward. I don’t have a choice in the matter.

According to the Astro Docs on Images, if I’m using the Astro Image component, I should put images in the src folder. However, if I’m using the img tag in HTML, the images need to be in the public folder. The problem is that I’m using React for every single page, and all the UI is built with React, Tailwind, and designed with… well, you get the idea. I can’t use the <Image> component in my React code without causing errors throughout the codebase.

So, there you have it. Large assets are now a big problem for me because performance and load times are suffering.

The Solution

The solution to my image woes is actually quite straightforward: use an image compressor. However, I’ve found that many image compressors out there have their own set of issues. Some are slow to load, others have limitations, and some can even degrade image quality to the point where it’s not worth using them. And then there are those that take an eternity to compress an image, turning a simple task into a frustrating experience.

As my image sizes continued to balloon and my site’s performance suffered, I decided to take matters into my own hands. I set out to build a new tool that would streamline my workflow and help me manage my images more efficiently. And so, Image Composer was born.

Image Composer

Image Composer is a tool I designed and built to fix the whole asset problem I was facing. I coded the entire app in just a single day — literally paused everything else (even my blog posts) and went full focus on it.

The goal was simple: I needed a way to compress images without trashing their quality, and maybe even add a little something extra — like rounded corners, because honestly, rounded images just look cleaner and more modern. And of course, the UI had to be simple and clean so that anyone could use it without needing a manual.

For the tech stack, I kept it super lightweight and fast:

  • Node.js server
  • EJS for template rendering
  • Tailwind CSS for styling

TLD’R: How it Works

If you don’t want to nerd out on the code, here’s the short version:

  • Visit the website
  • Drag and drop your images
  • Pick the quality you want
  • Choose if you want rounded corners
  • Hit “Compress Images.”
  • The server compresses them using a library called Sharp
  • The compressed images get displayed back in the UI
  • Hit “Download,” and boom — new, smaller, clean, high-quality images.

Now, Let’s Nerd Out: Full Code Breakdown

Here’s a full breakdown of the codebase — from starting the server all the way to the endpoint where the image gets uploaded, processed, and sent back to the user and much more.

1. app.js – The Entry Point

This is where everything starts.

const express = require("express");
const path = require("path");

const configureMiddleware = require("./config/middleware");
const configureViewEngine = require("./config/viewEngine");
const { notFoundHandler, globalErrorHandler } = require("./middleware/errorHandler");

const indexRoutes = require("./routes/routes");
const imageRoutes = require("./routes/image");

const app = express();
const port = process.env.PORT || 3000;

configureViewEngine(app);
configureMiddleware(app);

app.use("/", indexRoutes);
app.use("/", imageRoutes);

app.use(notFoundHandler);
app.use(globalErrorHandler);

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

What’s Happening Here?

  • We import Express and a few core modules.
  • We set up middleware (security, rate limiting, etc.).
  • We define routes (for the homepage + image compression API).
  • We handle errors properly.
  • We start the server and make it listen on port 3000 (or whatever’s in process.env.PORT).

Nothing fancy, just a solid Express setup.


2. config/middleware.js – Security & Performance Middleware

This file hardens the server and prevents abuse.

const express = require("express");
const helmet = require("helmet");
const rateLimit = require("express-rate-limit");
const hpp = require("hpp");
const cors = require("cors");
const path = require("path");

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  message: "Too many requests, try again later",
});

function configureMiddleware(app) {
  app.use(cors()); // Allow cross-origin requests
  app.use(hpp()); // Prevent HTTP param pollution
  app.use(limiter); // Prevent spam requests
  app.use(helmet()); // Security headers
  app.use(express.json());
  app.use(express.urlencoded({ extended: true }));
  app.use(express.static(path.join(__dirname, "..", "public")));
}

module.exports = configureMiddleware;

Why This Matters

  • Helmet adds security headers.
  • Rate limiting prevents abuse (max 100 requests per 15 mins per IP).
  • CORS is enabled so the API can be used from different origins.
  • Multer (used later) only allows JPEG, PNG, GIF, and WebP—nothing shady.

This keeps the server safe, fast, and reliable.


3. config/multer.js – File Upload Handling

Handles file uploads before passing them to Sharp.

const multer = require("multer");

const storage = multer.memoryStorage();

const upload = multer({
  storage: storage,
  limits: { fileSize: 30 * 1024 * 1024 },
  fileFilter: (req, file, cb) => {
    const allowedMimes = ["image/jpeg", "image/png", "image/gif", "image/webp"];
    allowedMimes.includes(file.mimetype) ? cb(null, true) : cb(new Error("Invalid file type"), false);
  },
});

module.exports = upload;

What’s Happening?

  • Stores images in memory instead of disk (faster processing).
  • Limits file size to 30MB (to prevent server crashes).
  • Blocks unsupported formats (JPEG, PNG, WebP, and GIF only).

Multer makes sure only valid images reach our processor.


4. controller/imageController.js – The Brain of the Operation

This is where Sharp does its magic.

const sharp = require("sharp");

async function compressImage(req, res, next) {
  if (!req.file) return res.status(400).send("No image uploaded.");

  try {
    const quality = Math.max(1, Math.min(100, parseInt(req.body.quality) || 80));
    const format = ["jpeg", "png", "webp"].includes(req.body.format) ? req.body.format : "jpeg";
    const rounded = req.body.roundedCorners === "true";
    
    let image = sharp(req.file.buffer).toFormat(format, { quality });

    if (rounded) {
      const metadata = await image.metadata();
      const mask = Buffer.from(`<svg><rect x="0" y="0" width="${metadata.width}" height="${metadata.height}" rx="50" ry="50"/></svg>`);
      image = image.composite([{ input: mask, blend: "dest-in" }]);
    }

    res.set("Content-Type", `image/${format}`);
    res.send(await image.toBuffer());
  } catch (error) {
    next(error);
  }
}

module.exports = { compressImage };

What’s Happening?

  • Validates inputs (quality: 1-100, formats: jpeg/png/webp).
  • Sharp compresses the image based on selected quality.
  • Optional rounded corners applied via SVG mask.
  • Sends back the optimized image—no saving to disk needed.

This keeps things fast and efficient.


5. routes/image.js – The API Endpoint

This is where the magic happens.

const express = require("express");
const upload = require("../config/multer");
const { compressImage } = require("../controller/imageController");

const router = express.Router();

router.post("/compress", upload.single("image"), compressImage);

module.exports = router;

How It Works

  • Receives an image via a POST request.
  • Passes it to Multer for validation.
  • Sends it to Sharp for compression.
  • Returns the optimized image.

And that’s it—one clean API route that just works.

Final Thoughts

Building this little app was honestly super fun, and I ended up learning way more than I expected. I had no idea image processing could be this much work! Most of my time actually went into getting the image compression to work properly — at first, it wasn’t working great at all. Designing the UI was the easy part compared to that.

If you want to try it out, you can check out the website — Image Composer — and if you’re curious about the code, it’s all up on GitHub too.

I even tested it in production! I compressed a bunch of big images into smaller ones, tweaked a few files in the codebase, removed some unnecessary JavaScript from my Astro blog site — and the performance score literally jumped from around 50% to 100%! Not kidding — you can check the Speed Insights yourself on Vercel where my site’s hosted.

Honestly, I can’t believe how much things improved just from building this one app. It’s easily the best thing I’ve built and deployed so far. I highly recommend giving the tool a try, and if you have any feedback or new ideas, feel free to open a pull request! I’d love to keep improving it.

Until then, peace out, nerds. 👓

Comments