En EDweekend tuvimos serios problemas de rendimiento que decidimos mejorar, antes nuestro método de rendering era SSR (Server Side Rendering) el cual por naturaleza del mismo hacia las peticiones necesarias para mostrar la información cada que un usuario entraba a la página, pero encontramos dos problemas.
- la página en ocaciones hacía peticiones innecesarias
- Había mucha deuda técnica
Cómo lo resolvimos? principalmente cambiando 2 cosas, la estructura de la aplicación y el método de rendering.
Estructura de la aplicación
Decidimos dividir nuestra página web en dos, ed.team y app.ed.team, en ed.team
está todo el contenido que puede ser público, es decir que no hay necesidad de iniciar sesión para poder interactuar en ella y en app.ed.team
está todo el contenido que sí necesita de un inicio de sesión para poder interactuar en ella, esto con el fin de aminorar la carga y dividir estrategias como el SEO, en app.ed.team
no hay necesidad de meter mucho SEO, ya que al final es una aplicación privada que necesita de un inicio de sesión para poder acceder a sus recursos, en cambio en ed.team
sí es necesario, ya que es la parte pública y es la que tiene el dominio que todos conocemos.
Método de rendering
Decidimos cambiar nuestra estrategia de renderizado a ISR (Incrementral Static Regeneration) el cual nos permitirá aminorar la carga de peticiones al servidor y al devolver sólo archivos estáticos con páginas ya generadas, la velocidad de la web aumentó y los tiempos de carga disminuyeron, pero qué es ISR?, bien lo explico.
Qué es ISR
ISR (Incremental Static Regeneration) es un método de rendering que te permite utilizar la generación estática por página sin necesidad de realizar un rebuild de todo la aplicación, es decir que las páginas estáticas pueden ser generadas en demanda en vez de solo al momento de hacer el build. Esto nos sirve mucho cuando queremos conservar las ventajas de los sitios estáticos en páginas que requieren información que es cambiante en un intervalo de tiempo.
Intervalo de tiempo
Cómo se mencionó las páginas son regeneradas en un intervalo de tiempo que nosotros le indicamos a cada página de Next.js, este tiempo puede ser 1 hora, 12 horas, 24 horas o el tiempo en el que necesites que se vuelva a generar la página, si este tiempo de revalidación no se le indica en una página esta permanecerá estática y sin cambios hasta que se vuelva a hacer otro build de la aplicación.
Cómo funciona ISR
Suponiendo que el tiempo de revalidación que establecemos es de 60 segundos.
- En tiempo de build nosotros generamos las páginas que le indiquemos a Next.js junto a su tiempo de revalidación y estas son cacheadas en el servidor
- En las peticiones a la página que suceden antes del tiempo de revalidación se responderá con las páginas que están cacheadas en el servidor
- Una vez que pase el tiempo de revalidación y se haga una petición, Next.js mostrará la página cacheada (obsoleta) y por debajo regenerará esa misma página
- Cuando la regeneración de la página haya concluido con éxito esta sobrescribirá la que estaba en caché (obsoleta), teniendo así la nueva página con los datos actualizados guardada en la caché del servidor y lista para ser mostrada en la siguiente petición.
Por qué EDteam lo usa
Nosotros necesitábamos una página web pública con un muy buen rendimiento que nos pudiera proporcionar un muy buen SEO, por lo cual decidimos crear desde cero la parte pública (sin sesión) del sitio web de EDteam, utilizando este método de rendering (ISR), ya que necesitábamos mantener las ventajas de las páginas estáticas para nuestro SEO y el buen rendimiento de la página.
Pero nuestros datos son muy cambiantes, en nuestra comunidad se publican posts de los usuarios al igual que en el blog por el equipo constantemente, se agregan y editan cursos, es decir que si nosotros queríamos mantener páginas estáticas, a cada cambio debíamos estar haciendo build para actualizar los datos, y es ahí donde entra ISR, nosotros solo le agregamos un tiempo de revalidación a las páginas que necesitamos que se estén regenerando y ya no nos preocupamos por que nuestras páginas estén obsoletas en cuanto a información
Cómo hacer un página con ISR
Set up del proyecto
Iniciamos una aplicación de next con create-next-app
1npx create-next-app --ts
Una vez instalada entramos a la carpeta del proyecto y corremos el comando npm run dev
y vemos nuestro proyecto en nuestro navegador.
Ahora, dentro de nuestra carpeta pages
creamos una carpeta llamada products
y dentro de ella creamos un archivo llamado [slug].tsx
Después, vamos a agregar las interfaces que necesitaremos para este ejemplo, creamos la carpeta interfaces
en el root de nuestro proyecto y dentro de la carpeta creamos el archivo product.interface.ts
1export interface Product { 2 id: number 3 title: string 4 price: number 5 description: string 6 category: string 7 image: string 8 rating: Rating 9} 10 11export interface Rating { 12 rate: number 13 count: number 14}
Y listo ya podemos empezar a desarrollar.
getStaticProps y getStaticPaths
Primero agregaremos el código necesario para el archivo index.tsx
, empezando por la función getStaticProps
1import type { GetStaticProps, NextPage } from 'next' 2import Link from 'next/link' 3import { Product } from '../interfaces/product.interface' 4 5const Home: NextPage<Props> = ({ products }) => { 6 return <main></main> 7} 8 9export const getStaticProps: GetStaticProps = async () => { 10 const res = await fetch('https://fakestoreapi.com/products') 11 const products = await res.json() 12 13 return { 14 props: { 15 products: products 16 } 17 } 18} 19 20interface Props { 21 products: Product[] 22} 23 24export default Home
En la función getStaticProps
estamos haciendo una petición hacia nuestra fakeapi que nos devuelve una lista de productos y retornamos las props que se le inyectarán a nuestro componente home. Esta funcion en desarrollo se ejecutará cada vez que se recargue la página, pero en tiempo de build se ejecutará para guardar la información en un archivo json y generar el html, teniendo así una página estática.
Ahora en nuestro componente home agregamos el marcado necesario
1const Home: NextPage<Props> = ({ products }) => { 2 return ( 3 <main> 4 <h1>Lista de productos</h1> 5 <ol> 6 {products.map(item => ( 7 <li key={item.id}> 8 <Link href="/products/[slug]" as={`/products/${item.id}`}> 9 <a>{item.title}</a> 10 </Link> 11 </li> 12 ))} 13 </ol> 14 </main> 15 ) 16}
Hasta el momento nuestra página debería lucir así
Ahora pasemos a realizar la página /products/[slug]
1import { GetStaticPaths, GetStaticProps, NextPage } from 'next' 2import { useRouter } from 'next/dist/client/router' 3import Image from 'next/image' 4import { Product } from '../../interfaces/product.interface' 5 6const ProductPage: NextPage<Props> = ({ product }) => { 7 return ( 8 <main> 9 <h1>{product.title}</h1> 10 <Image src={product.image} alt={product.title} width="200" height="200" layout="responsive" /> 11 </main> 12 ) 13} 14 15export const getStaticPaths: GetStaticPaths = async () => { 16 const res = await fetch('https://fakestoreapi.com/products?limit=10') 17 const first10Products = (await res.json()) as Product[] 18 19 // Lista de paginas que se renderizarán en tiempo de build 20 const paths = first10Products.map(item => ({ params: { slug: String(item.id) } })) 21 22 return { 23 paths, 24 fallback: true 25 } 26} 27 28export const getStaticProps: GetStaticProps = async ctx => { 29 const slug = ctx.params?.slug as string 30 31 const res = await fetch(`https://fakestoreapi.com/products/${slug}`) 32 const product = await res.json() 33 34 return { 35 props: { 36 product 37 } 38 } 39} 40 41interface Props { 42 product: Product 43} 44 45export default ProductPage
Como se puede ver en la línea 13 exportamos una función llamada getStaticPaths
La cual nos sirve para decirle a Next.js cuáles son las páginas dinámicas que debe renderizar a la hora de hacer build, en este caso le decimos que las páginas de los primeros 10 productos se deben de pre renderizar esto se lo indicamos retornando un objeto con la propiedad paths con la siguiente estructura
1paths: [ 2 { params: { ... } } 3],
Como pueden ver en el retorno de la función getStaticPaths
también retornamos un atributo llamado fallback
como true, esto quiere decir que cuando un usuario intente entrar a una página dinámica de la ruta /products
y esta no se generó en tiempo de build, mostrará un estado de loading mientras que por debajo Next.js genera la página estática. Si fallback fuese false, quiere decir que todas las páginas que no se hayan generado en tiempo de build retornarán un 404.
Para agregar este estado de loading mientras Next.js genera la página debemos hacerlo explícitamente agregando el siguiente código a nuestro componente ProductPage.
1const ProductPage: NextPage<Props> = ({ product }) => { 2 const router = useRouter() 3 4 if (router.isFallback) { 5 return <h1>Loading...</h1> 6 } 7 8 ... 9} 10 11...
Este estado de loading sólo aparecerá cuando por primera vez un usuario entre a la página, ya que no ha sido generada en tiempo de build, pero una vez que un usuario entre por primera vez, esta se generará y en las futuras peticiones se mostrará la página estática sin el estado de loading.
Hasta el momento nuestra página se debería ver así
Como podrán ver, si van a la url http://localhost:3000/products/1
no aparecerá el estado de loading, ya que esa fue una de las páginas que le indicamos a Next.js que generara en tiempo de build, en cambio si vamos a la url http://localhost:3000/products/11
sí veremos el estado de loading, ya que esta no fue unas de las páginas que le indicamos a Next.js que generara en tiempo de build.
ISR
Pero que pasa si un producto cambia sus datos cada x tiempo? o si a la lista de productos se le agrega otro más o se le quita, en ese caso tendríamos que volver a hacer el build del proyecto para que se vuelvan a generar las páginas con los datos nuevos, pero esto es poco práctico en algunos casos, para ello utilizaremos la estrategia ISR, agregando un tiempo de revalidación para que cada que pase ese tiempo y un usuario entre a esa página se vuelva a generar la página con los datos nuevos.
Para agregar ISR a una página en Next.js basta con agregar un atributo llamado revalidate
en el objeto que retorna la función getStaticProps
, en nuestro ejemplo modificaremos nuestros getStaticProps
en las dos páginas que tenemos index.ts
y /products/[slug].ts
index.tsx
1export const getStaticProps: GetStaticProps = async () => { 2 const ONE_HOUR = 3600 3 4 const res = await fetch('https://fakestoreapi.com/products') 5 const products = await res.json() 6 7 return { 8 props: { 9 products: products 10 }, 11 revalidate: ONE_HOUR 12 } 13}
Aquí le indicamos a Next.js que regenere la página cada 1 hora y que un usuario haya visitado la página después de ese tiempo de revalidación así como se explicó en la sección de Cómo funciona ISR
/products/[slug].tsx
1export const getStaticProps: GetStaticProps = async ctx => { 2 const ONE_DAY = 86400 3 4 const slug = ctx.params?.slug as string 5 6 const res = await fetch(`https://fakestoreapi.com/products/${slug}`) 7 const product = await res.json() 8 9 return { 10 props: { 11 product 12 }, 13 revalidate: ONE_DAY 14 } 15}
El tiempo de revalidación debe ser decidido por el equipo de desarrollo, ya que depende de que tanto cambian los productos o los contenidos de la página, por ejemplo en EDteam se le da un tiempo menor a la comunidad, ya que esta página está en constante cambio, usuarios dan like, comentan, publican y es muy fácil que en poco tiempo esta página se quede obsoleta, a diferencia del home, en el cual tenemos un tiempo un tanto más grande, ya que es una página que no muestra datos muy cambiantes.
Y así es como se crea una página con ISR y el por qué en EDteam decidimos usarlo, espero que este blog te haya sido de ayuda, haya aprendido o hayas reforzado tus conocimientos sobre ISR.
Si te han quedado dudas o quieres aportar al artículo puedes expresarte en la sección de los comentarios.