DEV Community

Cover image for Efficient Client-Side Image Preprocessing for AI Wrappers
NasajTools
NasajTools

Posted on

Efficient Client-Side Image Preprocessing for AI Wrappers

When we started building the AI Image-to-Text tool for NasajTools, we hit an immediate bottleneck: Latency.

Modern vision models (like GPT-4o or Claude 3.5 Sonnet) are incredibly powerful, but they are also sensitive to payload size. Users were uploading raw 4K screenshots or 10MB uncompressed photos directly from their phones. Sending these massive payloads to our serverless backend, and then proxying them to an AI provider, resulted in:

Slow user experience (waiting 5+ seconds just for the upload).

Timeouts on Vercel/AWS Lambda serverless functions (which often have 4.5MB payload limits).

Wasted Bandwidth costs.

We didn't need 4K resolution to extract text accurately. We needed a smart client-side pipeline to sanitize inputs before they ever touched our API.

The Problem
We needed a way to intercept the user's file selection, resize it to an "AI-friendly" dimension (usually max 2048px on the longest side), and compress it to a reasonable JPEG quality—all in the browser, without blocking the main thread.

Most developers simply FormData.append('file', file) and ship it. For high-traffic AI tools, that’s an architectural mistake.

The Code
We built a lightweight utility that utilizes the HTML5 API to resize and compress images on the fly. This logic runs entirely in the user's browser, turning a 10MB payload into a crisp ~300KB file in milliseconds.

Here is the core logic we use in production. It takes a raw File object and returns a Promise that resolves to a Blob ready for upload.

/**
 * Resizes and compresses an image file client-side.
 * @param {File} file - The original image file from the input.
 * @param {number} maxWidth - The maximum width allowed (e.g., 2048).
 * @param {number} quality - JPEG quality (0 to 1).
 * @returns {Promise<Blob>}
 */
export const optimizeImage = (file, maxWidth = 2048, quality = 0.8) => {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);

    reader.onload = (event) => {
      const img = new Image();
      img.src = event.target.result;

      img.onload = () => {
        const elem = document.createElement('canvas');
        let width = img.width;
        let height = img.height;

        // Calculate new dimensions while maintaining aspect ratio
        if (width > maxWidth) {
          height = Math.round(height * (maxWidth / width));
          width = maxWidth;
        }

        elem.width = width;
        elem.height = height;

        const ctx = elem.getContext('2d');
        ctx.drawImage(img, 0, 0, width, height);

        // Convert canvas to Blob (efficient binary format)
        ctx.canvas.toBlob(
          (blob) => {
            if (blob) {
              resolve(blob);
            } else {
              reject(new Error('Canvas compression failed.'));
            }
          },
          'image/jpeg',
          quality
        );
      };

      img.onerror = (error) => reject(error);
    };

    reader.onerror = (error) => reject(error);
  });
};
Enter fullscreen mode Exit fullscreen mode

Integrating it into the Upload Handler
In our React component, we use this utility to intercept the upload. Note how we handle the optimizing state to give the user feedback.

const handleFileUpload = async (event) => {
  const file = event.target.files[0];
  if (!file) return;

  setStatus('Optimizing image...');

  try {
    // 1. Client-side compression
    const optimizedBlob = await optimizeImage(file, 2048, 0.7);

    // 2. Prepare for upload
    const formData = new FormData();
    formData.append('file', optimizedBlob, 'optimized_image.jpg');

    setStatus('Processing with AI...');

    // 3. Send to our API
    const response = await fetch('/api/vision/extract-text', {
      method: 'POST',
      body: formData,
    });

    const data = await response.json();
    setTextResult(data.text);

  } catch (error) {
    console.error('Pipeline failed:', error);
    setStatus('Error processing image.');
  }
};
Enter fullscreen mode Exit fullscreen mode

Live Demo
You can see this pipeline in action (and inspect the network tab to see the reduced payload sizes) at our live tool: https://nasajtools.com

Try uploading a massive, high-res photo. You’ll notice the upload step is nearly instant because we aren't sending the heavy original file.

Performance Considerations
By moving this logic to the client, we reduced our average API request body size by 94%. This had cascading benefits:

Faster Inference: AI models process smaller images faster (fewer tokens/pixels to analyze).

Cheaper Bills: We pay less for egress bandwidth.

Better UX: Users on poor 4G connections can still use the tool effectively.

When building AI wrappers, remember that the "AI" part is only half the battle. The data delivery pipeline is where the real engineering happens.

Top comments (0)