Vom Vite-Stack zum Next.js 16 — was beim Rewrite wirklich brach
Drei Wochen Migration, ein neuer Stack, ein paar gute Lehren: was beim Wechsel von Vite + Tailwind auf Next.js 16 mit Once UI tatsächlich Reibung produziert hat — und was überraschend reibungslos lief.
Diese Site lief bis vor drei Wochen auf einem klassischen Vite-Stack: React 18, Tailwind CSS, React Router, ein Haufen handgepflegter Komponenten. Sie funktionierte. Aber jeder neue Bereich bedeutete zwei Stunden Layout-Mathe, und das eingebaute SEO bestand aus einem einsamen react-helmet-Wrapper.
Drei Wochen später ist sie auf Next.js 16 mit Once UI 1.7, URL-basierter i18n via next-intl und Server Components als Default. In diesem Post: was im Migrationsweg unerwartet kostete — und was so reibungslos lief, dass ich den Stack-Wechsel im Rückblick deutlich früher gemacht hätte.
Warum überhaupt migrieren?
Drei Gründe, hart priorisiert:
- SEO mit Substanz. Vite-React-SPAs rendern clientseitig. Google rendert mittlerweile JS, aber Bing, DuckDuckGo und einige andere weniger gut. Server Components liefern HTML, das jeder Crawler ohne Umweg liest.
- i18n als Erstklass-Bürger. URL-basierte Lokalisierung (
/de,/en) ohne ein eigenes Routing-Konstrukt war auf React Router immer ein Hack. Mit next-intl ist es ein einziger Middleware-Eintrag. - Bilder, Schriften, Assets.
next/imageundnext/fontnehmen einen Großteil der Performance-Optimierung ab, ohne dass ich an einer Webpack-Config drehe.
Was tatsächlich brach
Die ehrlichen Stolpersteine — in der Reihenfolge, in der ich auf sie traf.
1. Server Components erkennen die "use client"-Grenze nicht automatisch
Mit useState, useEffect oder einem onClick-Handler wandert eine Komponente automatisch in den Client-Bundle — sofern sie selbst "use client" deklariert hat oder von einer solchen Komponente importiert wird. Vergisst man die Direktive, gibt Next.js einen Build-Fehler aus, und der Stacktrace zeigt oft nur die innerste Komponente, nicht die Stelle der eigenen Code-Grenze.
Die Lehre: pro Sektion eine klare Trennung. Bei mir endet die Server-Welt am <Pricing>-Wrapper; alles innerhalb ist Client. Diese harte Linie hat den Migrations-Tag gerettet.
2. Theme-Init-Skripte und nested Layouts
Damit Once UI ohne Aufflackern (FOUC) startet, muss vor der Hydration ein winziges Inline-Script das richtige data-theme-Attribut auf <html> setzen. In meinem ersten Setup lebte das Script im [locale]/layout.tsx — direkt neben <html>.
Beim Sprachwechsel produzierte React eine Warnung („encountered a script tag while rendering a React component"), und Next.js warnte zusätzlich gegen strategy="beforeInteractive" in nested Layouts. Lösung: html, body und das Init-Script gehören in das Root-Layout (app/layout.tsx). Das [locale]/layout.tsx wird zur reinen Content-Schicht und setzt das lang-Attribut über ein kleines Client-Effect-Komponent dynamisch.
Das ist eine der Lektionen, die in keinem Blogpost auftaucht, aber jeden Next.js-16-Migrant trifft, der mehr als ein Locale unterstützt.
3. Tailwind raus, SCSS-Module rein
Once UI macht keine Annahmen über die Styling-Schicht. Ich hätte Tailwind weiter verwenden können — habe mich aber für SCSS-Module entschieden, weil Tailwind in Kombination mit Once UIs eigenen data-*-Selektoren anstrengend wird.
Migrationsaufwand: einmal alles. Jeder einzelne className="flex items-center gap-4" wurde zu einem strukturierten <Row gap="16"> oder einer eigenen .row-Klasse im SCSS-Modul der Sektion. Hat eine ganze Woche gekostet, aber die Codebase liest sich heute deutlich klarer — Markup wird wieder Inhaltsstruktur, nicht Stylelogik.
4. once-ui Spacing-Tokens sind streng
Spacing-Props akzeptieren nur konkrete Stufen (0, 1, 2, 4, 8, 12, 16, 20, 24, 32, 40, 48, 56, 64, 80, 104, 128, 160). Eine einfache gap="6" wirft einen TypeScript-Fehler. Anfangs nervig, im Rückblick richtig: das verhindert die schleichende Spacing-Erosion, die jede gewachsene Codebase irgendwann hat.
5. Responsive Props sind nicht überall reaktiv
Once UIs hide-Prop und ähnliche bedingte Props funktionieren — aber an Breakpoints, die nicht immer dem entsprechen, was ich erwartet habe (insbesondere bei meinem Header zwischen Desktop und Mobile). Pragmatische Lösung: für zwei Stellen reine CSS-Media-Queries geschrieben. Schneller, als die Library-Internals nachzubauen.
6. next/intl + Locale-Routing
Die Migration des Routings selbst war erstaunlich angenehm. Aber: die generateStaticParams-Pflicht für jeden dynamischen Locale-Segment-Pfad ist leicht zu übersehen. Ohne sie funktioniert die Site im Dev, aber npm run build bricht ab.
Was überraschend reibungslos lief
Die Bilder. next/image hat jedes einzelne Bild kommentarlos auf AVIF + WebP umgestellt. Lighthouse Performance ging dadurch von 84 auf 99 — bei null Konfigurationsaufwand.
Die Fonts. next/font/google liefert Schriften lokal aus, ohne dass eine Verbindung zu Google-Servern nötig ist. Das ist nicht nur DSGVO-freundlich — es eliminiert auch das Layout-Shift-Problem, das Vite mit FontFaceObserver bekommen hat.
Die SEO-Metadaten. Das Metadata-API von Next.js ist erwachsen geworden: generateMetadata liest die Locale aus dem Param, generiert pro Seite Title + Description + OG + hreflang-Alternativen. Das ist Boilerplate, den man einmal schreibt und nie wieder anfasst.
JSON-LD Schema. Per Server Component direkt im Layout gerendert. Service-, FAQPage- und BreadcrumbList-Schemas auf jeder Audience-Landing-Page sind drei Zeilen <Script type="application/ld+json"> — zur Laufzeit unsichtbar, im Search-Console-Bericht sehr sichtbar.
Was ich anders machen würde
Früher anfangen, html und body in das Root-Layout zu legen. Ich habe das initial im [locale]/layout.tsx gemacht, weil das die intuitive Stelle ist und der Code-Beispiel in der next-intl-Doku dort lebt. Der Fehler kostete einen halben Tag, weil mehrere Probleme (Theme-Init, JSON-LD-Skript, Hydration) erst mit der richtigen Struktur sauber lösbar waren.
Den Header früher von Once UIs Responsive-Props entkoppeln. Ich hätte mir zwei Tage Reibung erspart, wenn ich von Anfang an reine Media-Queries genutzt hätte — gerade für so eine zentrale Komponente.
Frontmatter-basiertes Blog-System früher bauen. Ich hatte zuerst Markdown-Posts via remark angedacht, bin dann auf TSX-Dateien mit Default-Export umgeschwenkt — weniger Abhängigkeiten, mehr Flexibilität. Der Post, den du gerade liest, ist genau eine solche Datei.
Würde ich es nochmal machen?
Ohne Zögern. Vite + React 18 + Tailwind ist ein hervorragender Stack für Apps, die hauptsächlich client-seitig leben. Für eine Marketing- oder Portfolio-Site, bei der SEO, i18n und wartbare Strukturen wichtiger sind als gefühlte Bundle-Größe, ist Next.js 16 mit Once UI das nahezu konkurrenzlose Werkzeug.
Drei Wochen Investment, ein Stack, mit dem ich die nächsten zwei Jahre arbeiten will, und eine Site, die ich nicht alle drei Monate neu refactor-en werde. Das ist ein Trade ich gerne wieder eingehe.
Der Code dieser Site ist privat — aber wenn du an einer ähnlichen Migration sitzt und an einer der hier genannten Stellen feststeckst, schreib mich an. Eine halbe Stunde Pair-Programming spart die zwei Tage, in denen ich diese Stolpersteine selbst gefunden habe.