Introducción/Guías/Migración/Vite

Cómo migrar de Vite a Next.js

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 cambiar de Vite a Next.js:

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 varias razones:

  1. El navegador necesita esperar a que el código de React y todo el paquete de tu aplicación se descarguen y ejecuten antes de que tu código pueda enviar solicitudes para cargar algunos datos.
  2. El código de tu aplicación crece con cada nueva característica y dependencia adicional que agregues.

Sin división de código automática

El problema anterior de tiempos de carga lentos puede manejarse parcialmente con 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 de código automática integrada en su enrutador.

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.

Si bien Next.js admite la obtención de datos en el cliente, también te da la opción de mover la obtención de datos al servidor, lo que puede eliminar las cascadas cliente-servidor.

Estados de carga rápidos e intencionales

Con soporte integrado para transmisión mediante React Suspense, 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 eliminar cambios de diseño.

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.

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.

Optimizaciones integradas

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.

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 las 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 los 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 compilación a `./dist/`.
}

export default nextConfig

Bueno saber: Puedes usar tanto .js como .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 estás usando TypeScript, puedes omitir este paso.

  1. Elimina la referencia del 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 a true: "esModuleInterop": true
  6. Establece jsx a preserve: "jsx": "preserve"
  7. Establece allowJs a true: "allowJs": true
  8. Establece forceConsistentCasingInFileNames a true: "forceConsistentCasingInFileNames": true
  9. Establece incremental a true: "incremental": true

Aquí tienes 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 la configuración de 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 de 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 carpeta src.
  2. Crea un nuevo archivo layout.tsx dentro de ese directorio app:
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return '...'
}

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

  1. Copia el contenido de tu archivo index.html en el componente <RootLayout> creado anteriormente, reemplazando 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>
  )
}
  1. Next.js ya incluye por defecto las etiquetas meta charset y meta viewport, por lo que puedes eliminarlas 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>
  )
}
  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 coloques en el nivel superior del directorio app. Después de mover todos los archivos admitidos al directorio app, puedes eliminar 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>
  )
}
  1. Finalmente, Next.js puede gestionar 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>
  )
}

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 el SEO y la capacidad de compartir tus páginas en la web.

Paso 5: Crear la página de entrada (Entrypoint)

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

  1. Crea un directorio [[...slug]] dentro de tu directorio app.

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

Este directorio es lo que se conoce como un segmento de ruta opcional catch-all. Next.js utiliza un enrutador basado en el sistema de archivos donde las carpetas se usan para definir rutas. Este directorio especial asegurará que todas las rutas de tu aplicación sean dirigidas al archivo page.tsx que contiene.

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

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

export default function Page() {
  return '...' // Actualizaremos esto
}

Nota importante: Se pueden usar las extensiones .js, .jsx o .tsx para los archivos de Página.

Este archivo es un Componente del Servidor (Server Component). Cuando ejecutas next build, el archivo se prerenderiza en un recurso estático. No requiere ningún código dinámico.

Este archivo importa nuestro CSS global y le indica a generateStaticParams que solo generaremos una ruta, la ruta de índice en /.

Ahora, movamos el resto de nuestra aplicación Vite que se ejecutará solo en el cliente.

'use client'

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

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

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

Este archivo es un Componente del Cliente (Client Component), definido por la directiva 'use client'. Los Componentes del Cliente aún se prerenderizan a HTML en el servidor antes de enviarse al cliente.

Como queremos comenzar con una aplicación solo del cliente, podemos configurar Next.js para desactivar el prerenderizado desde el componente App hacia abajo.

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

Ahora, actualiza tu página de entrada para usar el nuevo componente:

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

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

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

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

Next.js maneja las importaciones de imágenes estáticas de manera 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 está configurada como auto, la dimensión tomará el valor predeterminado 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. Luego, opcionalmente, puedes migrar al componente <Image> para aprovechar la optimización de imágenes configurando un loader, o migrando al servidor predeterminado de Next.js que tiene optimización automática de imágenes.

  1. Convierte 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. Pasa 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} />

Alternativamente, puedes referenciar la URL pública del recurso de imagen basado en el nombre del archivo. Por ejemplo, public/logo.png servirá la imagen en /logo.png para tu aplicación, que sería el valor de src.

Advertencia: Si estás usando TypeScript, podrías encontrar errores de tipo al acceder a la propiedad src. Puedes ignorarlos de manera segura por ahora. 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, que 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. Añade lo siguiente a tu archivo .env:
.env
# ...
NEXT_PUBLIC_BASE_PATH="/some-base-path"
  1. Configura 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 Página Única (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 a `/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 migraste exitosamente a Next.js. Pero antes, necesitas actualizar los scripts en tu package.json con comandos relacionados a Next.js, y añadir .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
dist

Ahora ejecuta npm run dev y abre http://localhost:3000. Deberías ver tu aplicación ejecutándose ahora en Next.js.

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

Paso 9: Limpieza

Ahora puedes limpiar tu 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 página única. 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: