Migración 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 cambiar de Vite a Next.js:
Tiempo de carga inicial lento
Si has construido tu aplicación con el plugin predeterminado 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:
- 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.
- El código de tu aplicación crece con cada nueva característica y dependencia adicional que agregues.
No hay división de código automática
El problema anterior de tiempos de carga lentos puede manejarse en cierta medida 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 (network waterfalls) al dividir el código manualmente. Next.js proporciona división de código automática integrada en su enrutador.
Cascadas de red (Network waterfalls)
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 de red 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 deseas 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 (layout shifts).
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 datos 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 y 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 para la 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 debes hacer es instalar next
como dependencia:
npm install next@latest
Paso 2: Crear el archivo de configuración de Next.js
Crea un archivo next.config.mjs
en la raíz de tu proyecto. Este archivo contendrá tus opciones de configuración de Next.js.
/** @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
Nota importante: 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 usas TypeScript, puedes omitir este paso.
- Elimina la referencia al proyecto a
tsconfig.node.json
- Agrega
./dist/types/**/*.ts
y./next-env.d.ts
al arregloinclude
- Agrega
./node_modules
al arregloexclude
- Agrega
{ "name": "next" }
al arregloplugins
encompilerOptions
:"plugins": [{ "name": "next" }]
- Establece
esModuleInterop
entrue
:"esModuleInterop": true
- Establece
jsx
enpreserve
:"jsx": "preserve"
- Establece
allowJs
entrue
:"allowJs": true
- Establece
forceConsistentCasingInFileNames
entrue
:"forceConsistentCasingInFileNames": true
- Establece
incremental
entrue
:"incremental": true
Aquí tienes un ejemplo de un archivo tsconfig.json
funcional con esos cambios:
{
"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 (Root Layout)
Una aplicación de App Router de Next.js debe incluir un archivo de diseño raíz, que es un componente de servidor de 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:
- Crea un nuevo directorio
app
dentro de tu directoriosrc
. - Crea un nuevo archivo
layout.tsx
dentro de ese directorioapp
:
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return null
}
export default function RootLayout({ children }) {
return null
}
Nota importante: Puedes usar las extensiones
.js
,.jsx
o.tsx
para los archivos de diseño.
- Copia el contenido de tu archivo
index.html
en el componente<RootLayout>
creado anteriormente, reemplazando las etiquetasbody.div#root
ybody.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>
)
}
- 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>
)
}
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>
)
}
- 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 directorioapp
. Después de mover todos los archivos admitidos al directorioapp
, 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>
)
}
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>
)
}
- Finalmente, Next.js puede gestionar tus últimas etiquetas
<head>
con la API de Metadatos. Mueve tu información de metadatos final a un objetometadata
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, has pasado 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 a este archivo en Vite es el archivo main.tsx
. En este paso, configurarás el punto de entrada de tu aplicación.
- Crea un directorio
[[...slug]]
dentro del directorioapp
.
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 del directorio app
.
Este directorio es lo que se conoce como un segmento de ruta opcional catch-all. Next.js utiliza 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 se dirijan al archivo page.tsx
que contiene.
- Crea un nuevo archivo
page.tsx
dentro del directorioapp/[[...slug]]
con el siguiente contenido:
import '../../index.css'
export function generateStaticParams() {
return [{ slug: [''] }]
}
export default function Page() {
return '...' // Actualizaremos esto más adelante
}
import '../../index.css'
export function generateStaticParams() {
return [{ slug: [''] }]
}
export default function Page() {
return '...' // Actualizaremos esto más adelante
}
Nota importante: Se pueden usar extensiones
.js
,.jsx
o.tsx
para los archivos de página.
Este archivo es un Componente de Servidor (Server Component). Cuando ejecutas next build
, el archivo se prerrenderiza en un recurso estático. No requiere 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 />
}
'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 de Cliente (Client Component), definido por la directiva 'use client'
. Los Componentes de Cliente aún se prerrenderizan a HTML en el servidor antes de enviarse al cliente.
Como queremos comenzar con una aplicación solo para cliente, podemos configurar Next.js para desactivar el prerrenderizado 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 />
}
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:
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 beneficios adicionales como la optimización automática de imágenes. El componente <Image>
establece automáticamente los atributos width
y height
del <img>
resultante según 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á 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. 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.
- Convierte las rutas de importación absoluta para imágenes importadas desde
/public
en importaciones relativas:
// Antes
import logo from '/logo.png'
// Después
import logo from '../public/logo.png'
- 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 hacer referencia a 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, es posible que encuentres 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 .env
similar a Vite. La principal diferencia es el prefijo utilizado para exponer variables de entorno en el lado del cliente.
- Cambia todas las variables de entorno con el prefijo
VITE_
aNEXT_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.MODE
⇒process.env.NODE_ENV
import.meta.env.PROD
⇒process.env.NODE_ENV === 'production'
import.meta.env.DEV
⇒process.env.NODE_ENV !== 'production'
import.meta.env.SSR
⇒typeof window !== 'undefined'
Next.js tampoco proporciona una variable de entorno BASE_URL
integrada. Sin embargo, aún puedes configurar una si la necesitas:
- Agrega lo siguiente a tu archivo
.env
:
# ...
NEXT_PUBLIC_BASE_PATH="/some-base-path"
- Establece
basePath
comoprocess.env.NEXT_PUBLIC_BASE_PATH
en tu archivonext.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 del build a `./dist/`.
basePath: process.env.NEXT_PUBLIC_BASE_PATH, // Establece la ruta base como `/some-base-path`.
}
export default nextConfig
- Actualiza los usos de
import.meta.env.BASE_URL
aprocess.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, 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
:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}
# ...
.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 pull request 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 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:
- Migrar de React Router al Enrutador de Aplicación (App Router) de Next.js para obtener:
- División de código automática
- Renderizado del lado del servidor con streaming
- Componentes de Servidor de React
- Optimizar imágenes con el componente
<Image>
- Optimizar fuentes con
next/font
- Optimizar scripts de terceros con el componente
<Script>
- Actualizar tu configuración de ESLint para soportar las reglas de Next.js