I Built a Free Brat Generator - Here's What I Learned About Next.js Performance published
Source: Dev.to

The brat aesthetic is simple by design — bold lowercase text, solid‑color background, nothing else. Building a tool around that simplicity turned out to be more interesting than I expected.
I built ibratgenerator.com — a free brat‑style image generator inspired by Charli XCX’s album aesthetic. Here is what the build taught me about Next.js performance, canvas rendering, and SEO.
What the Tool Does
Users open the page, type any text, pick a background colour, and download a high‑resolution PNG. No signup, no watermark, no account needed. The core stack is Next.js 16 App Router with a canvas‑based rendering engine written in vanilla TypeScript.
The tool supports:
- Custom background and text colours
- Aspect‑ratio presets (1:1, 4:5, 9:16, 16:9)
- Stickers and emoji overlays
- Typography controls — font size, letter spacing, alignment
- Export up to 3000 px PNG
- Full mobile‑touch support
The Next.js Dynamic Import Problem
The canvas component is entirely client‑side — it uses browser APIs that do not exist on the server. So I loaded it with next/dynamic and ssr: false:
const BratGeneratorLazy = dynamic(
() => import('./BratGenerator'),
{ ssr: false }
);
This works fine on mobile. But on desktop, Google was crawling the page and seeing a large blank space where the tool should be. The LCP element had no reserved space, so the page layout shifted when the component loaded.
Fix
Wrap the lazy component in a container that reserves space:
The position: relative and width: 100% matter — without them the stacking context breaks and the component can overlap the sticky header on scroll. This single change improved desktop position significantly in Google Search Console.
Canvas Performance — History Snapshots
The tool has undo/redo support. Every user interaction pushes a state snapshot to a history array. The original implementation cloned the background image on every snapshot:
// Before — wrong
bgImage: s.bgImage
? (() => {
const img = new Image();
img.src = s.bgImage!.src;
return img;
})()
: null,
Creating a new HTMLImageElement on every keystroke causes heap churn and UI‑thread stutters — especially noticeable on mobile.
Fix
Reference the existing image instead of cloning it:
// After — correct
bgImage: s.bgImage,
The image object is static between snapshots. Copying the reference is safe and eliminates the unnecessary DOM instantiation on every interaction.
Pointer Event Cleanup
Global pointer listeners were added to window for sticker‑drag handling:
window.addEventListener('pointermove', onPointerMove);
window.addEventListener('pointerup', onPointerUp);
window.addEventListener('pointercancel', onPointerUp);
In React StrictMode, components mount twice in development. Without explicit cleanup before adding listeners, you get duplicate handlers that cause double‑trigger bugs on drag release.
Fix
Remove any existing listeners before adding new ones:
// Remove before adding to prevent duplicates
window.removeEventListener('pointermove', onPointerMove);
window.removeEventListener('pointerup', onPointerUp);
window.removeEventListener('pointercancel', onPointerUp);
window.addEventListener('pointermove', onPointerMove);
window.addEventListener('pointerup', onPointerUp);
window.addEventListener('pointercancel', onPointerUp);
Content Security Policy with Next.js
Adding CSP headers in next.config.ts broke Microsoft Clarity because the script loads from scripts.clarity.ms but sends data to t.clarity.ms — two different subdomains that both need to be allow‑listed:
// script-src needs scripts.clarity.ms
// connect-src needs t.clarity.ms
// Both subdomains required — just clarity.ms is not enough
Lesson: Always check the actual network requests in DevTools after adding CSP headers. The console error messages tell you exactly which domain is being blocked.
What I Would Do Differently
The multilingual routing I added early on (/[lang]/ dynamic segment) caused a TypeScript build error after I removed the routes but forgot to clear the .next cache. Stale types in .next/dev/types/validator.ts kept referencing the deleted route.
Fix: Delete the .next directory entirely and run a fresh build. Simple, but it saved a lot of debugging time. Clear your cache whenever you make structural routing changes.
The Tool Is Live
Brat Generator — free, no signup, watermark‑free PNG export. Works on mobile and desktop.
If you are building canvas‑based tools in Next.js, the dynamic import + reserved‑space pattern is worth keeping in your toolkit. The LCP improvement from that single wrapper “ was more significant than I expected.
Built with Next.js 16, TypeScript, and vanilla canvas.
- Next.js 16, TypeScript, and vanilla Canvas API. Deployed on Vercel.*
