Migration von Vite

Diese Anleitung hilft Ihnen, eine bestehende Vite-Anwendung zu Next.js zu migrieren.

Gründe für den Wechsel

Es gibt mehrere Gründe, warum Sie von Vite zu Next.js wechseln möchten:

Langsame anfängliche Ladezeit der Seite

Wenn Sie Ihre Anwendung mit dem Standard-Vite-Plugin für React erstellt haben, handelt es sich um eine rein clientseitige Anwendung. Rein clientseitige Anwendungen, auch bekannt als Single-Page Applications (SPAs), haben oft eine langsame anfängliche Ladezeit. Dies geschieht aus folgenden Gründen:

  1. Der Browser muss warten, bis der React-Code und Ihr gesamtes Anwendungs-Bundle heruntergeladen und ausgeführt wurden, bevor Ihr Code Anfragen zum Laden von Daten senden kann.
  2. Ihr Anwendungscode wächst mit jedem neuen Feature und jeder zusätzlichen Abhängigkeit, die Sie hinzufügen.

Kein automatisches Code-Splitting

Das vorherige Problem der langsamen Ladezeiten kann teilweise mit Code-Splitting behoben werden. Wenn Sie jedoch versuchen, Code-Splitting manuell durchzuführen, verschlechtern Sie oft die Performance. Es ist einfach, unbeabsichtigt Netzwerk-Wasserfälle (network waterfalls) einzuführen, wenn Sie Code-Splitting manuell durchführen. Next.js bietet automatisches Code-Splitting, das in seinen Router integriert ist.

Netzwerk-Wasserfälle

Eine häufige Ursache für schlechte Performance tritt auf, wenn Anwendungen sequenzielle Client-Server-Anfragen zum Abrufen von Daten stellen. Ein gängiges Muster für das Abrufen von Daten in einer SPA ist das anfängliche Rendern eines Platzhalters und das anschließende Abrufen von Daten, nachdem die Komponente gemountet wurde. Leider bedeutet dies, dass eine untergeordnete Komponente, die Daten abruft, erst mit dem Abruf beginnen kann, nachdem die übergeordnete Komponente ihre eigenen Daten vollständig geladen hat.

Während das Abrufen von Daten auf dem Client mit Next.js unterstützt wird, haben Sie auch die Möglichkeit, das Abrufen von Daten auf den Server zu verlagern, was Client-Server-Wasserfälle eliminieren kann.

Schnelle und gezielte Ladezustände

Mit integrierter Unterstützung für Streaming durch React Suspense können Sie gezielter festlegen, welche Teile Ihrer Benutzeroberfläche zuerst und in welcher Reihenfolge geladen werden sollen, ohne Netzwerk-Wasserfälle einzuführen.

Dies ermöglicht es Ihnen, Seiten zu erstellen, die schneller laden und Layoutverschiebungen vermeiden.

Wahl der Datenabrufstrategie

Je nach Bedarf ermöglicht Next.js Ihnen, Ihre Datenabrufstrategie auf Seiten- und Komponentenbasis zu wählen. Sie können entscheiden, ob Sie Daten zum Build-Zeitpunkt, bei der Anfrage auf dem Server oder auf dem Client abrufen möchten. Beispielsweise können Sie Daten von Ihrem CMS abrufen und Ihre Blogbeiträge zum Build-Zeitpunkt rendern, die dann effizient auf einem CDN zwischengespeichert werden können.

Middleware

Next.js Middleware ermöglicht es Ihnen, Code auf dem Server auszuführen, bevor eine Anfrage abgeschlossen ist. Dies ist besonders nützlich, um zu vermeiden, dass nicht authentifizierte Inhalte kurz angezeigt werden, wenn der Benutzer eine nur für authentifizierte Benutzer zugängliche Seite besucht, indem der Benutzer zu einer Login-Seite umgeleitet wird. Die Middleware ist auch nützlich für Experimente und Internationalisierung.

Integrierte Optimierungen

Bilder, Schriftarten und Skripte von Drittanbietern haben oft einen erheblichen Einfluss auf die Performance einer Anwendung. Next.js kommt mit integrierten Komponenten, die diese automatisch für Sie optimieren.

Migrationsschritte

Unser Ziel bei dieser Migration ist es, so schnell wie möglich eine funktionierende Next.js-Anwendung zu erhalten, damit Sie dann schrittweise Next.js-Features einführen können. Zunächst behalten wir sie als rein clientseitige Anwendung (SPA) bei, ohne Ihren bestehenden Router zu migrieren. Dies hilft, die Wahrscheinlichkeit von Problemen während des Migrationsprozesses zu minimieren und Merge-Konflikte zu reduzieren.

Schritt 1: Installieren der Next.js-Abhängigkeit

Als Erstes müssen Sie next als Abhängigkeit installieren:

Terminal
npm install next@latest

Schritt 2: Erstellen der Next.js-Konfigurationsdatei

Erstellen Sie eine next.config.mjs im Stammverzeichnis Ihres Projekts. Diese Datei enthält Ihre Next.js-Konfigurationsoptionen.

next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export', // Erzeugt eine Single-Page Application (SPA).
  distDir: './dist', // Ändert das Build-Ausgabeverzeichnis zu `./dist/`.
}

export default nextConfig

Gut zu wissen: Sie können entweder .js oder .mjs für Ihre Next.js-Konfigurationsdatei verwenden.

Schritt 3: TypeScript-Konfiguration aktualisieren

Falls Sie TypeScript verwenden, müssen Sie Ihre tsconfig.json-Datei mit den folgenden Änderungen aktualisieren, um sie mit Next.js kompatibel zu machen. Falls Sie TypeScript nicht verwenden, können Sie diesen Schritt überspringen.

  1. Entfernen Sie die Projektreferenz zu tsconfig.node.json
  2. Fügen Sie ./dist/types/**/*.ts und ./next-env.d.ts zum include-Array hinzu
  3. Fügen Sie ./node_modules zum exclude-Array hinzu
  4. Fügen Sie { "name": "next" } zum plugins-Array in compilerOptions hinzu: "plugins": [{ "name": "next" }]
  5. Setzen Sie esModuleInterop auf true: "esModuleInterop": true
  6. Setzen Sie jsx auf preserve: "jsx": "preserve"
  7. Setzen Sie allowJs auf true: "allowJs": true
  8. Setzen Sie forceConsistentCasingInFileNames auf true: "forceConsistentCasingInFileNames": true
  9. Setzen Sie incremental auf true: "incremental": true

Hier ein Beispiel für eine funktionierende tsconfig.json mit diesen Änderungen:

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "allowJs": true,
    "forceConsistentCasingInFileNames": true,
    "incremental": true,
    "plugins": [{ "name": "next" }]
  },
  "include": ["./src", "./dist/types/**/*.ts", "./next-env.d.ts"],
  "exclude": ["./node_modules"]
}

Weitere Informationen zur Konfiguration von TypeScript finden Sie in der Next.js-Dokumentation.

Schritt 4: Erstellen des Root-Layouts

Eine Next.js-App Router-Anwendung muss eine Root-Layout-Datei enthalten, die eine React Server Component ist und alle Seiten Ihrer Anwendung umschließt. Diese Datei wird auf der obersten Ebene des app-Verzeichnisses definiert.

Die nächste Entsprechung zur Root-Layout-Datei in einer Vite-Anwendung ist die index.html-Datei, die Ihre <html>, <head> und <body>-Tags enthält.

In diesem Schritt konvertieren Sie Ihre index.html-Datei in eine Root-Layout-Datei:

  1. Erstellen Sie ein neues app-Verzeichnis in Ihrem src-Verzeichnis.
  2. Erstellen Sie eine neue layout.tsx-Datei in diesem app-Verzeichnis:
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return null
}
export default function RootLayout({ children }) {
  return null
}

Gut zu wissen: Für Layout-Dateien können die Erweiterungen .js, .jsx oder .tsx verwendet werden.

  1. Kopieren Sie den Inhalt Ihrer index.html-Datei in die zuvor erstellte <RootLayout>-Komponente und ersetzen Sie dabei die body.div#root und body.script-Tags durch <div id="root">{children}</div>:
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <link rel="icon" type="image/svg+xml" href="/icon.svg" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <link rel="icon" type="image/svg+xml" href="/icon.svg" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
  1. Next.js enthält standardmäßig bereits die meta charset- und meta viewport-Tags, sodass Sie diese sicher aus Ihrem <head> entfernen können:
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <link rel="icon" type="image/svg+xml" href="/icon.svg" />
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <link rel="icon" type="image/svg+xml" href="/icon.svg" />
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
  1. Alle Metadaten-Dateien wie favicon.ico, icon.png, robots.txt werden automatisch zum <head>-Tag der Anwendung hinzugefügt, solange Sie sie auf der obersten Ebene des app-Verzeichnisses ablegen. Nachdem Sie alle unterstützten Dateien in das app-Verzeichnis verschoben haben, können Sie deren <link>-Tags sicher entfernen:
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <title>My App</title>
        <meta name="description" content="My App is a..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
  1. Schließlich kann Next.js Ihre letzten <head>-Tags mit der Metadata API verwalten. Verschieben Sie Ihre letzten Metadaten-Informationen in ein exportiertes metadata-Objekt:
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'My App',
  description: 'My App is a...',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
export const metadata = {
  title: 'My App',
  description: 'My App is a...',
}

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}

Mit den oben genannten Änderungen sind Sie von der Deklaration aller Inhalte in Ihrer index.html zu dem konventionsbasierten Ansatz von Next.js übergegangen, der in das Framework integriert ist (Metadata API). Dieser Ansatz ermöglicht es Ihnen, die SEO und die Web-Sharing-Fähigkeit Ihrer Seiten einfacher zu verbessern.

Schritt 5: Erstellen der Entrypoint-Seite

In Next.js definieren Sie einen Entrypoint für Ihre Anwendung, indem Sie eine page.tsx-Datei erstellen. Die engste Entsprechung dieser Datei in Vite ist Ihre main.tsx-Datei. In diesem Schritt richten Sie den Entrypoint Ihrer Anwendung ein.

  1. Erstellen Sie ein [[...slug]]-Verzeichnis in Ihrem app-Verzeichnis.

Da wir in dieser Anleitung zunächst Next.js als SPA (Single Page Application) einrichten möchten, muss Ihr Seiten-Entrypoint alle möglichen Routen Ihrer Anwendung abfangen. Dazu erstellen Sie ein neues [[...slug]]-Verzeichnis im app-Verzeichnis.

Dieses Verzeichnis ist ein sogenanntes optionales Catch-All-Routen-Segment. Next.js verwendet einen dateisystembasierten Router, bei dem Verzeichnisse zur Definition von Routen verwendet werden. Dieses spezielle Verzeichnis stellt sicher, dass alle Routen Ihrer Anwendung zur darin enthaltenen page.tsx-Datei weitergeleitet werden.

  1. Erstellen Sie eine neue page.tsx-Datei im app/[[...slug]]-Verzeichnis mit folgendem Inhalt:
import '../../index.css'

export function generateStaticParams() {
  return [{ slug: [''] }]
}

export default function Page() {
  return '...' // Wir werden dies später aktualisieren
}
import '../../index.css'

export function generateStaticParams() {
  return [{ slug: [''] }]
}

export default function Page() {
  return '...' // Wir werden dies später aktualisieren
}

Gut zu wissen: Für Seiten-Dateien können die Erweiterungen .js, .jsx oder .tsx verwendet werden.

Diese Datei ist eine Server-Komponente (Server Component). Wenn Sie next build ausführen, wird die Datei in ein statisches Asset vorge-rendert. Sie benötigt keinen dynamischen Code.

Diese Datei importiert unser globales CSS und teilt generateStaticParams mit, dass wir nur eine Route generieren werden, nämlich die Index-Route unter /.

Nun übernehmen wir den Rest unserer Vite-Anwendung, die nur clientseitig ausgeführt wird.

'use client'

import React from 'react'
import dynamic from 'next/dynamic'

const App = dynamic(() => import('../../App'), { ssr: false })

export function ClientOnly() {
  return <App />
}
'use client'

import React from 'react'
import dynamic from 'next/dynamic'

const App = dynamic(() => import('../../App'), { ssr: false })

export function ClientOnly() {
  return <App />
}

Diese Datei ist eine Client-Komponente (Client Component), definiert durch die 'use client'-Direktive. Client-Komponenten werden dennoch auf dem Server zu HTML vorge-rendert, bevor sie an den Client gesendet werden.

Da wir zunächst eine reine Client-Anwendung möchten, können wir Next.js so konfigurieren, dass das Pre-Rendering ab der App-Komponente deaktiviert wird.

const App = dynamic(() => import('../../App'), { ssr: false })

Aktualisieren Sie nun Ihren Entrypoint, um die neue Komponente zu verwenden:

import '../../index.css'
import { ClientOnly } from './client'

export function generateStaticParams() {
  return [{ slug: [''] }]
}

export default function Page() {
  return <ClientOnly />
}
import '../../index.css'
import { ClientOnly } from './client'

export function generateStaticParams() {
  return [{ slug: [''] }]
}

export default function Page() {
  return <ClientOnly />
}

Schritt 6: Statische Bild-Imports anpassen

Next.js behandelt statische Bild-Imports etwas anders als Vite. Bei Vite gibt der Import einer Bilddatei deren öffentliche URL als Zeichenkette zurück:

App.tsx
import image from './img.png' // `image` wird in der Produktion '/assets/img.2d8efhg.png' sein

export default function App() {
  return <img src={image} />
}

Bei Next.js geben statische Bild-Imports ein Objekt zurück. Dieses Objekt kann dann direkt mit der Next.js <Image>-Komponente verwendet werden, oder Sie können die src-Eigenschaft des Objekts mit Ihrem bestehenden <img>-Tag nutzen.

Die <Image>-Komponente bietet zusätzliche Vorteile wie automatische Bildoptimierung. Die <Image>-Komponente setzt automatisch die width- und height-Attribute des resultierenden <img> basierend auf den Abmessungen des Bildes. Dies verhindert Layoutverschiebungen beim Laden des Bildes. Allerdings kann dies Probleme verursachen, wenn Ihre Anwendung Bilder enthält, bei denen nur eine der Dimensionen gestylt ist, ohne dass die andere auf auto gesetzt ist. Wenn nicht auf auto gesetzt, wird die Dimension standardmäßig auf den Wert des <img>-Dimensionsattributs gesetzt, was zu verzerrten Bildern führen kann.

Die Beibehaltung des <img>-Tags reduziert die Anzahl der Änderungen in Ihrer Anwendung und verhindert die oben genannten Probleme. Sie können später optional auf die <Image>-Komponente migrieren, um von der Optimierung der Bilder zu profitieren, indem Sie einen Loader konfigurieren, oder zum standardmäßigen Next.js-Server wechseln, der automatische Bildoptimierung bietet.

  1. Konvertieren Sie absolute Importpfade für Bilder aus /public in relative Imports:
// Vorher
import logo from '/logo.png'

// Nachher
import logo from '../public/logo.png'
  1. Übergeben Sie die src-Eigenschaft des Bildes anstelle des gesamten Bildobjekts an Ihr <img>-Tag:
// Vorher
<img src={logo} />

// Nachher
<img src={logo.src} />

Alternativ können Sie die öffentliche URL für das Bildasset basierend auf dem Dateinamen referenzieren. Beispielsweise wird public/logo.png das Bild unter /logo.png für Ihre Anwendung bereitstellen, was dann der src-Wert wäre.

Warnung: Wenn Sie TypeScript verwenden, könnten Sie Typfehler beim Zugriff auf die src-Eigenschaft erhalten. Sie können diese vorerst ignorieren. Sie werden am Ende dieser Anleitung behoben.

Schritt 7: Umgebungvariablen migrieren

Next.js unterstützt .env-Umgebungsvariablen ähnlich wie Vite. Der Hauptunterschied liegt im Präfix, das verwendet wird, um Umgebungsvariablen clientseitig verfügbar zu machen.

  • Ändern Sie alle Umgebungsvariablen mit dem Präfix VITE_ zu NEXT_PUBLIC_.

Vite stellt einige integrierte Umgebungsvariablen im speziellen import.meta.env-Objekt bereit, die von Next.js nicht unterstützt werden. Sie müssen deren Verwendung wie folgt anpassen:

  • import.meta.env.MODEprocess.env.NODE_ENV
  • import.meta.env.PRODprocess.env.NODE_ENV === 'production'
  • import.meta.env.DEVprocess.env.NODE_ENV !== 'production'
  • import.meta.env.SSRtypeof window !== 'undefined'

Next.js bietet auch keine integrierte BASE_URL-Umgebungsvariable. Sie können jedoch eine konfigurieren, falls benötigt:

  1. Fügen Sie Folgendes zu Ihrer .env-Datei hinzu:
.env
# ...
NEXT_PUBLIC_BASE_PATH="/some-base-path"
  1. Setzen Sie basePath auf process.env.NEXT_PUBLIC_BASE_PATH in Ihrer next.config.mjs-Datei:
next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export', // Erzeugt eine Single-Page Application (SPA).
  distDir: './dist', // Ändert das Build-Ausgabeverzeichnis zu `./dist/`.
  basePath: process.env.NEXT_PUBLIC_BASE_PATH, // Setzt den Basispfad auf `/some-base-path`.
}

export default nextConfig
  1. Aktualisieren Sie Verwendungen von import.meta.env.BASE_URL zu process.env.NEXT_PUBLIC_BASE_PATH

Schritt 8: Skripte in package.json aktualisieren

Sie sollten nun in der Lage sein, Ihre Anwendung auszuführen, um zu testen, ob die Migration zu Next.js erfolgreich war. Zuvor müssen Sie jedoch die scripts in Ihrer package.json mit Next.js-bezogenen Befehlen aktualisieren und .next sowie next-env.d.ts zu Ihrer .gitignore hinzufügen:

package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  }
}
.gitignore
# ...
.next
next-env.d.ts
dist

Führen Sie nun npm run dev aus und öffnen Sie http://localhost:3000. Sie sollten Ihre Anwendung nun mit Next.js laufen sehen.

Beispiel: Sehen Sie sich diesen Pull Request für ein funktionierendes Beispiel einer zu Next.js migrierten Vite-Anwendung an.

Schritt 9: Bereinigung

Sie können nun Ihren Codebase von Vite-bezogenen Artefakten bereinigen:

  • Löschen Sie main.tsx
  • Löschen Sie index.html
  • Löschen Sie vite-env.d.ts
  • Löschen Sie tsconfig.node.json
  • Löschen Sie vite.config.ts
  • Deinstallieren Sie Vite-Abhängigkeiten

Nächste Schritte

Wenn alles nach Plan verlaufen ist, haben Sie nun eine funktionierende Next.js-Anwendung, die als Single-Page-Anwendung läuft. Allerdings nutzen Sie noch nicht die meisten Vorteile von Next.js, aber Sie können nun schrittweise Änderungen vornehmen, um alle Vorteile zu nutzen. Hier ist, was Sie als Nächstes tun könnten: