Fetching latest headlines…
I Built a Free Brat Generator - Here's What I Learned About Next.js Performance published
NORTH AMERICA
πŸ‡ΊπŸ‡Έ United Statesβ€’May 21, 2026

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

0 views0 likes0 comments
Originally published byDev.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 color, 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 colors
  • 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 3000px 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:

tsxconst 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.

The fix was a simple wrapper:

tsx<div style={{ 
  minHeight: '520px', 
  position: 'relative', 
  width: '100%' 
}}>
  <BratGeneratorLazy />
</div>

That single change improved desktop position significantly in Google Search Console. The position: relative and width: 100% matter β€” without them the stacking context breaks and the component can overlap the sticky header on scroll.
Canvas Performance β€” History Snapshots
The tool has undo/redo support. Every user interaction pushes a state snapshot to a history array. The original implementation was cloning the background image on every snapshot:

typescript// 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. The fix is direct reference assignment:

typescript// 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:

typescriptwindow.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 event handlers that cause double-trigger bugs on drag release. The fix:
typescript// 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 allowlisted:

typescript// script-src needs scripts.clarity.ms
// connect-src needs t.clarity.ms
// Both subdomains required β€” just clarity.ms is not enough

The lesson: always check the actual network requests in DevTools after adding CSP headers. The error messages in the console 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. The stale types in .next/dev/types/validator.ts kept referencing the deleted route.
The fix was deleting the .next directory entirely and running a fresh build. Simple, but it cost debugging time. Clear your cache when 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 div was more significant than I expected.

Built with Next.js 16, TypeScript, and vanilla Canvas API. Deployed on Vercel.

Comments (0)

Sign in to join the discussion

Be the first to comment!