Skip to main content

Command Palette

Search for a command to run...

Just-In-Time Image Generation: How I Slashed Curse.You Response Times from 3 Seconds to 300ms

Updated
6 min read
Just-In-Time Image Generation: How I Slashed Curse.You Response Times from 3 Seconds to 300ms

The Challenge: Instant, Shareable Content

In the hyper-fast world of social media, speed is everything. When a link is shared, you have a fraction of a second to capture attention. My tool, Curse.You, generates personalized meme images on the fly, which automatically unfurl into rich previews on platforms like Discord, Twitter, and Telegram.

The core challenge? I needed to generate and serve a unique, custom image in the brief moment a social media crawler scrapes our URL. These crawlers are impatient; they'll time out in as little as two seconds. My initial attempts were far too slow, clocking in at over 3 seconds.

This is the story of how I battled cold starts and rendering bottlenecks on our journey to an average 300ms response time, all powered by Cloudflare Workers.

The Road to 300ms: A Three-Act Drama

My quest for speed was an iterative process of building, failing, and learning. Each approach taught me a critical lesson about performance in a serverless edge environment.

Act I: The High-Quality Dream with Rust + WASM (Time: 3+ Seconds)

My first attempt aimed for perfection. I chose Rust for its performance and resvg for its superb SVG rendering quality, compiling the entire package to WebAssembly (WASM) to run on Cloudflare Workers.

I have used the Resvg stack before, in WASM, and have it live on NitroQR right now. What could go wrong, right? Well, I actually utilise tiny-skia, the rendering engine subset of skia that was built for Resvg. The overheads between the two apparently look wildly different, even with the difference in usage accounted for.

The Theory: Combine the speed of Rust with the quality of a professional-grade SVG renderer.

The Reality: The overhead was crushing.

  • WASM Initialization: ~800ms

  • resvg Rendering: ~2,000ms

  • Total Time: A sluggish ~2,850ms

The Downfall:

  • WASM Cold Starts: The initialization cost on each request was a massive bottleneck.

  • Desktop-Grade Libraries: resvg is a powerful tool, but it's optimized for environments with more memory and processing power, not the constrained sandbox of a serverless worker.

  • Bundle Size: The complex dependency tree resulted in a large bundle, further slowing things down.

Lesson #1: The theoretical performance of a language doesn't matter if the environment's overhead negates the benefits.

Act II: The JavaScript Pivot with SVG Libraries (Time: 1.5+ Seconds)

Reasoning that a native JavaScript solution would avoid WASM's overhead, I switched to popular NPM libraries for SVG-to-PNG conversion.

The Theory: Stick to the native runtime (JavaScript) and leverage the rich Node.js ecosystem.

The Reality: I cut our time in half, but it was still far too slow.

  • Library Loading: ~200ms

  • Canvas Rendering: ~1,200ms

  • Total Time: A middling ~1,530ms

The Downfall:

  • Hidden Complexities: Most libraries use a canvas implementation under the hood, which has its own performance limitations within the Workers environment.

  • Heavy Dependencies: Pulling in large libraries designed for browsers or Node.js introduced significant parsing and memory overhead.

Lesson #2: "Off-the-shelf" doesn't mean "optimized for the edge." Serverless functions require purpose-built or minimalistic dependencies.

Act III: The Breakthrough with Direct Raster Generation (Time: 300ms)

Frustrated with high-level abstractions, I went back to first principles. What if I abandoned SVG entirely and just... built the image pixel by pixel?

The Theory: Eliminate all layers of abstraction—no SVG parsing, no complex layout engines, no external libraries. Manipulate a raw pixel buffer directly.

The Reality: A resounding success.

  • Pixel Buffer Creation: ~80ms

  • Text Rendering (Custom): ~120ms

  • PNG Encoding (UPNG.js): ~60ms

  • R2 Upload: ~35ms

  • Total Time: A blazing-fast ~300ms

The Win: By controlling every pixel, I eliminated all bottlenecks. The code was simple, had zero dependencies for the core generation logic, and was perfectly suited for the constrained serverless environment. While this means it only supports a-z right now, that is good enough for an MVP.

Lesson #3: For maximum performance, solve the simplest version of your problem. We didn't need a full graphics engine; we just needed to place colored rectangles on a screen.

How It Works: Building an Image from Scratch

My final solution is built around a RasterGenerator class that operates directly on a pixel buffer.

The Core Strategy:

  1. Allocate Memory: Create a Uint8ClampedArray, which is a simple array representing all the RGBA (Red, Green, Blue, Alpha) values for every pixel in the image.

  2. Draw Manually: Implement our own basic functions to draw rectangles and text by directly changing the values in the array.

  3. Render Text with a Bitmap Font: The secret sauce was creating our own simple bitmap font. Instead of parsing font files and calculating glyphs, we just store characters as arrays of 1s and 0s. This is incredibly fast and has zero overhead.

  4. Encode to PNG: Use the lightweight and highly efficient UPNG.js library to compress the final pixel buffer into a PNG file.

This approach completely bypasses the need for complex, CPU-intensive graphics libraries.

Key Architectural Decisions

Beyond the generation logic, several other components were critical for achieving speed and reliability.

Outsmarting the Cache with a Two-Step Redirect

Social media platforms cache aggressively. If a user shares curse.you/michael, the crawler might see a cached version. To ensure a fresh image every time, we use a redirect flow:

  1. Initial Request: https://curse.you/michael

  2. Server Response: A temporary redirect (HTTP 307) to a unique URL: https://curse.you/michael/a9b4c1d8

  3. Final Page: The unique URL serves the HTML with OpenGraph tags pointing to the freshly generated image.

This forces the crawler to treat every share as a unique page, defeating its cache while allowing the final generated image asset to be cached permanently on our CDN. The next step is to make this a persistent url, so that it serves the same curse every time. Soon!

Building the Fortress: Security & Abuse Prevention

A tool that generates content from user input is a prime target for abuse. The defense is multi-layered:

  • Verified Crawler Allowlist: We only perform on-demand generation for crawlers we trust (e.g., Discord, Twitter).

  • Aggressive Rate Limiting: All other traffic faces strict limits.

  • Content Filtering: We block known spam patterns and restricted words.

  • Cloudflare WAF: We leverage Cloudflare's Web Application Firewall for an extra layer of protection.

The Stack That Makes It Possible

  • Runtime: Cloudflare Workers provide the global, low-latency compute environment. V8 isolates mean near-zero cold start times.

  • Storage: Cloudflare R2 is perfect for serving images. With zero egress fees and really small file sizes, it's incredibly cost-effective for viral content.

  • Language: Modern JavaScript (ES2022) with zero-dependency generation logic.

The Results: By the Numbers

After deploying the direct raster approach, monitoring speaks for itself:

MetricPerformance
Average Response Time287ms
P95 Response Time420ms
Error Rate0.03%
Image Cache Hit Rate97.2%

Final Lessons Learned

This journey from 3 seconds to 300ms reinforced a fundamental principle of software engineering: simplicity scales.

  1. Embrace Constraints: The limitations of the serverless environment forced me to find a more efficient solution. What started as a problem became my greatest catalyst for innovation.

  2. Question Abstractions: High-level libraries are great for rapid development, but they often hide performance costs. Don't be afraid to go "down a level" and build a custom solution when speed is critical.

  3. The Right Tool for the Job: Rust/WASM is a phenomenal technology, but it wasn't the right fit for this specific problem. The lean, dependency-free JavaScript solution proved to be the winner.

By ditching complex graphics engines for direct pixel manipulation, I built a system that is not only 10x faster but also more robust, secure, and cost-effective at global scale.