Migrar desde Vite

Esta guía te ayudará a migrar una aplicación existente de Vite a Next.js.

¿Por qué cambiar?

Existen varias razones por las que podrías querer migrar de Vite a Next.js:

  1. Tiempo de carga inicial lento: Si has construido tu aplicación con el plugin por defecto de Vite para React, tu aplicación es puramente del lado del cliente. Las aplicaciones solo del lado del cliente, también conocidas como aplicaciones de una sola página (SPAs), suelen experimentar tiempos de carga inicial lentos. Esto ocurre por un par de razones:
    1. El navegador necesita esperar a que el código de React y todo tu paquete de aplicación se descarguen y ejecuten antes de que tu código pueda enviar solicitudes para cargar algunos datos.
    2. Tu código de aplicación crece con cada nueva característica y dependencia adicional que agregas.
  2. Sin división automática de código: El problema anterior de tiempos de carga lentos puede manejarse parcialmente con la división de código. Sin embargo, si intentas hacer la división de código manualmente, a menudo empeorarás el rendimiento. Es fácil introducir inadvertidamente cascadas de red al dividir el código manualmente. Next.js proporciona división automática de código integrada en su enrutador.
  3. Cascadas de red: Una causa común de bajo rendimiento ocurre cuando las aplicaciones hacen solicitudes secuenciales cliente-servidor para obtener datos. Un patrón común para la obtención de datos en una SPA es renderizar inicialmente un marcador de posición y luego obtener los datos después de que el componente se haya montado. Desafortunadamente, esto significa que un componente hijo que obtiene datos no puede comenzar a obtenerlos hasta que el componente padre haya terminado de cargar sus propios datos. En Next.js, este problema se resuelve obteniendo datos en Componentes del Servidor.
  4. Estados de carga rápidos e intencionales: Gracias al soporte integrado para Transmisión con Suspense, con Next.js puedes ser más intencional sobre qué partes de tu interfaz de usuario quieres cargar primero y en qué orden sin introducir cascadas de red. Esto te permite construir páginas que se cargan más rápido y también eliminar cambios de diseño.
  5. Elige la estrategia de obtención de datos: Dependiendo de tus necesidades, Next.js te permite elegir tu estrategia de obtención de datos por página y componente. Puedes decidir obtener en tiempo de construcción, en tiempo de solicitud en el servidor o en el cliente. Por ejemplo, puedes obtener datos de tu CMS y renderizar tus publicaciones de blog en tiempo de construcción, que luego pueden almacenarse en caché eficientemente en una CDN.
  6. Middleware: El Middleware de Next.js te permite ejecutar código en el servidor antes de que se complete una solicitud. Esto es especialmente útil para evitar mostrar contenido no autenticado cuando el usuario visita una página solo para autenticados redirigiéndolo a una página de inicio de sesión. El middleware también es útil para experimentación e internacionalización.
  7. Optimizaciones integradas: Las imágenes, fuentes y scripts de terceros suelen tener un impacto significativo en el rendimiento de una aplicación. Next.js viene con componentes integrados que los optimizan automáticamente por ti.

Pasos de migración

Nuestro objetivo con esta migración es obtener una aplicación Next.js funcional lo más rápido posible, para que luego puedas adoptar características de Next.js incrementalmente. Para empezar, la mantendremos como una aplicación puramente del lado del cliente (SPA) sin migrar tu enrutador existente. Esto ayuda a minimizar las posibilidades de encontrar problemas durante el proceso de migración y reduce conflictos de fusión.

Paso 1: Instalar la dependencia de Next.js

Lo primero que necesitas hacer es instalar next como dependencia:

Terminal
npm install next@latest

Paso 2: Crear el archivo de configuración de Next.js

Crea un next.config.mjs en la raíz de tu proyecto. Este archivo contendrá tus opciones de configuración de Next.js.

next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export', // Genera una Aplicación de una Sola Página (SPA).
  distDir: './dist', // Cambia el directorio de salida de la construcción a `./dist/`.
}

export default nextConfig

Bueno saber: Puedes usar .js o .mjs para tu archivo de configuración de Next.js.

Paso 3: Actualizar la configuración de TypeScript

Si estás usando TypeScript, necesitas actualizar tu archivo tsconfig.json con los siguientes cambios para hacerlo compatible con Next.js. Si no usas TypeScript, puedes saltarte este paso.

  1. Elimina la referencia de proyecto a tsconfig.node.json
  2. Agrega ./dist/types/**/*.ts y ./next-env.d.ts al arreglo include
  3. Agrega ./node_modules al arreglo exclude
  4. Agrega { "name": "next" } al arreglo plugins en compilerOptions: "plugins": [{ "name": "next" }]
  5. Establece esModuleInterop en true: "esModuleInterop": true
  6. Establece jsx en preserve: "jsx": "preserve"
  7. Establece allowJs en true: "allowJs": true
  8. Establece forceConsistentCasingInFileNames en true: "forceConsistentCasingInFileNames": true
  9. Establece incremental en true: "incremental": true

Aquí un ejemplo de un tsconfig.json funcional con esos cambios:

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"]
}

Puedes encontrar más información sobre configurar TypeScript en la documentación de Next.js.

Paso 4: Crear el diseño raíz

Una aplicación de App Router de Next.js debe incluir un archivo de diseño raíz, que es un Componente del Servidor React que envolverá todas las páginas de tu aplicación. Este archivo se define en el nivel superior del directorio app.

El equivalente más cercano al archivo de diseño raíz en una aplicación Vite es el archivo index.html, que contiene tus etiquetas <html>, <head> y <body>.

En este paso, convertirás tu archivo index.html en un archivo de diseño raíz:

  1. Crea un nuevo directorio app en tu directorio src.
  2. Crea un nuevo archivo layout.tsx dentro de ese directorio app:
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return null
}
export default function RootLayout({ children }) {
  return null
}

Bueno saber: Se pueden usar extensiones .js, .jsx o .tsx para archivos de Layout.

  1. Copia el contenido de tu archivo index.html en el componente <RootLayout> previamente creado mientras reemplazas las etiquetas body.div#root y body.script con <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 ya incluye por defecto las etiquetas meta charset y meta viewport, así que puedes eliminarlas con seguridad de tu <head>:
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. Cualquier archivo de metadatos como favicon.ico, icon.png, robots.txt se agrega automáticamente a la etiqueta <head> de la aplicación siempre que los hayas colocado en el nivel superior del directorio app. Después de mover todos los archivos soportados al directorio app, puedes eliminar con seguridad sus etiquetas <link>:
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. Finalmente, Next.js puede manejar tus últimas etiquetas <head> con la API de Metadatos. Mueve tu información final de metadatos a un objeto metadata exportado:
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>
  )
}

Con los cambios anteriores, pasaste de declarar todo en tu index.html a usar el enfoque basado en convenciones de Next.js integrado en el framework (API de Metadatos). Este enfoque te permite mejorar más fácilmente tu SEO y la capacidad de compartir tus páginas en la web.

Paso 5: Crear la página de entrada

En Next.js declaras un punto de entrada para tu aplicación creando un archivo page.tsx. El equivalente más cercano de este archivo en Vite es tu archivo main.tsx. En este paso, configurarás el punto de entrada de tu aplicación.

  1. Crea un directorio [[...slug]] en tu directorio app.

Como en esta guía nuestro objetivo primero es configurar nuestro Next.js como una SPA (Aplicación de una Sola Página), necesitas que tu punto de entrada de página capture todas las rutas posibles de tu aplicación. Para eso, crea un nuevo directorio [[...slug]] en tu directorio app.

Este directorio es lo que se llama un segmento de ruta catch-all opcional. Next.js usa un enrutador basado en sistema de archivos donde los directorios se usan para definir rutas. Este directorio especial asegurará que todas las rutas de tu aplicación sean dirigidas a su archivo page.tsx contenido.

  1. Crea un nuevo archivo page.tsx dentro del directorio app/[[...slug]] con el siguiente contenido:
'use client'

import dynamic from 'next/dynamic'
import '../../index.css'

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

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

import dynamic from 'next/dynamic'
import '../../index.css'

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

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

Bueno saber: Se pueden usar extensiones .js, .jsx o .tsx para archivos de Página.

Este archivo contiene un componente <Page> que está marcado como un Componente del Cliente por la directiva 'use client'. Sin esa directiva, el componente habría sido un Componente del Servidor.

En Next.js, los Componentes del Cliente son pre-renderizados a HTML en el servidor antes de ser enviados al cliente, pero como primero queremos tener una aplicación puramente del lado del cliente, necesitas decirle a Next.js que deshabilite el pre-renderizado para el componente <App> importándolo dinámicamente con la opción ssr establecida en false:

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

Paso 6: Actualizar las importaciones de imágenes estáticas

Next.js maneja las importaciones de imágenes estáticas de forma ligeramente diferente a Vite. Con Vite, importar un archivo de imagen devuelve su URL pública como una cadena:

App.tsx
import image from './img.png' // `image` será '/assets/img.2d8efhg.png' en producción

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

Con Next.js, las importaciones de imágenes estáticas devuelven un objeto. Este objeto puede usarse directamente con el componente <Image> de Next.js, o puedes usar la propiedad src del objeto con tu etiqueta <img> existente.

El componente <Image> tiene los beneficios adicionales de la optimización automática de imágenes. El componente <Image> establece automáticamente los atributos width y height del <img> resultante basándose en las dimensiones de la imagen. Esto evita cambios de diseño cuando la imagen se carga. Sin embargo, esto puede causar problemas si tu aplicación contiene imágenes con solo una de sus dimensiones estilizada sin que la otra esté configurada como auto. Cuando no se establece como auto, la dimensión tomará por defecto el valor del atributo de dimensión del <img>, lo que puede hacer que la imagen aparezca distorsionada.

Mantener la etiqueta <img> reducirá la cantidad de cambios en tu aplicación y evitará los problemas mencionados. No obstante, más adelante deberías migrar al componente <Image> para aprovechar las optimizaciones automáticas.

  1. Convertir las rutas de importación absolutas para imágenes importadas desde /public en importaciones relativas:
// Antes
import logo from '/logo.png'

// Después
import logo from '../public/logo.png'
  1. Pasar la propiedad src de la imagen en lugar del objeto completo a tu etiqueta <img>:
// Antes
<img src={logo} />

// Después
<img src={logo.src} />

Advertencia: Si estás usando TypeScript, podrías encontrar errores de tipo al acceder a la propiedad src. Por ahora, puedes ignorarlos sin problemas. Se solucionarán al final de esta guía.

Paso 7: Migrar las variables de entorno

Next.js tiene soporte para variables de entorno en archivos .env, similar a Vite. La principal diferencia es el prefijo usado para exponer variables de entorno en el lado del cliente.

  • Cambia todas las variables de entorno con el prefijo VITE_ a NEXT_PUBLIC_.

Vite expone algunas variables de entorno integradas en el objeto especial import.meta.env, las cuales no son compatibles con Next.js. Debes actualizar su uso de la siguiente manera:

  • 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 tampoco proporciona una variable de entorno BASE_URL integrada. Sin embargo, aún puedes configurar una si la necesitas:

  1. Agrega lo siguiente a tu archivo .env:
.env
# ...
NEXT_PUBLIC_BASE_PATH="/some-base-path"
  1. Establece basePath como process.env.NEXT_PUBLIC_BASE_PATH en tu archivo next.config.mjs:
next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export', // Genera una aplicación de una sola página (SPA).
  distDir: './dist', // Cambia el directorio de salida de la compilación a `./dist/`.
  basePath: process.env.NEXT_PUBLIC_BASE_PATH, // Establece la ruta base como `/some-base-path`.
}

export default nextConfig
  1. Actualiza los usos de import.meta.env.BASE_URL a process.env.NEXT_PUBLIC_BASE_PATH

Paso 8: Actualizar los scripts en package.json

Ahora deberías poder ejecutar tu aplicación para probar si la migración a Next.js fue exitosa. Pero antes, debes actualizar los scripts en tu package.json con los comandos relacionados con Next.js, y agregar .next y next-env.d.ts a tu .gitignore:

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

Ahora ejecuta npm run dev y abre http://localhost:3000. Con suerte, deberías ver tu aplicación funcionando en Next.js.

Si tu aplicación seguía una configuración convencional de Vite, esto es todo lo que necesitarías hacer para tener una versión funcional de tu aplicación.

Ejemplo: Revisa esta solicitud de extracción para ver un ejemplo funcional de una aplicación Vite migrada a Next.js.

Paso 9: Limpieza

Ahora puedes limpiar tu base de código de los artefactos relacionados con Vite:

  • Elimina main.tsx
  • Elimina index.html
  • Elimina vite-env.d.ts
  • Elimina tsconfig.node.json
  • Elimina vite.config.ts
  • Desinstala las dependencias de Vite

Próximos pasos

Si todo salió según lo planeado, ahora tienes una aplicación Next.js funcional que se ejecuta como una aplicación de una sola página. Sin embargo, aún no estás aprovechando la mayoría de los beneficios de Next.js, pero ahora puedes comenzar a hacer cambios incrementales para obtenerlos. Esto es lo que podrías hacer a continuación: