| ... | ... | @@ -0,0 +1,868 @@ |
| 1 | import Heading from "@/blog/tags/heading.tsx"; |
| 2 | import TableOfContents from "@/blog/tags/table-of-contents.tsx"; |
| 3 | import { Layout } from "@/blog/tags/layout.tsx"; |
| 4 | export { theme } from "@/blog/tags/layout.tsx"; |
| 5 | |
| 6 | export const meta = { |
| 7 | title: "Un año con el enrutador de aplicación (App Router) de Next.js — por qué decidimos cambiar", |
| 8 | description: "Una crítica de los componentes de servidor de React y Next.js 15.", |
| 9 | keywords: ["webdev", "technical analysis", "opinion"], |
| 10 | authors: ["clover caruso"], |
| 11 | embed: { |
| 12 | thumbnail: "/open-graph/next-js.es.png" |
| 13 | }, |
| 14 | twitter: { |
| 15 | image: "https://paperclover.net/open-graph/next-js.png" |
| 16 | }, |
| 17 | canonical: "/blog/webdev/one-year-next-app-router.es" |
| 18 | }; |
| 19 | |
| 20 | <Layout |
| 21 | meta={meta} |
| 22 | date={'Oct 21st, 2025'} |
| 23 | slug="webdev/one-year-next-app-router" |
| 24 | tags={meta.keywords} |
| 25 | translation={{ |
| 26 | lang: "es", |
| 27 | author: "@trubiso", |
| 28 | href: null, |
| 29 | date: "Jan 13th, 2026" |
| 30 | }} |
| 31 | > |
| 32 | |
| 33 | He estado usando [Next.js] profesionalmente para el desarrollo de aplicaciones |
| 34 | web en mi trabajo y el diseño de su enrutador de aplicación (App Router) y los |
| 35 | [componentes de servidor (React Server Components/RSC)][rsc] me parece |
| 36 | extremadamente frustrante a nivel fundamental. No es por pequeños bugs o porque |
| 37 | la API me parezca confusa, sino por grandes discrepancias en las decisiones de |
| 38 | diseño fundamentales que tomaron Vercel y el equipo de React en su creación. |
| 39 | |
| 40 | Cuanto más asisto a eventos de desarrollo web, más veo a gente a quien no le |
| 41 | gusta Next.js, pero que aún así debe usarlo. Al final del artículo, compartiré |
| 42 | cómo escapamos de este infierno, migrando todo nuestro frontend a [TanStack |
| 43 | Start] sin problemas. |
| 44 | |
| 45 | [Next.js]: https://nextjs.org |
| 46 | [rsc]: https://react.dev/reference/rsc/server-components |
| 47 | |
| 48 | <TableOfContents> |
| 49 | |
| 50 | - [Un repaso técnico: ¿qué son los componentes de servidor?][§1] |
| 51 | - [Los inconvenientes del enrutador de aplicaciones][§2] |
| 52 | - [Las actualizaciones optimistas son imposibles][§2.1] |
| 53 | - [Cada navegación supone una petición][§2.2] |
| 54 | - [Los layouts tienen restricciones artificiales][§2.3] |
| 55 | - [Te llevas todo el contenido dos veces de todas formas][§2.4] |
| 56 | - [Turbopack da asco][§2.5] |
| 57 | - [Dejando atrás Next.js y Vercel en el trabajo][§3] |
| 58 | - [`next/metadata` es maravilloso][§3.1] |
| 59 | - [`next/og` está bien también][§3.2] |
| 60 | - [Mi experiencia parece ser la típica][§4] |
| 61 | - [Opta por herramientas que te respeten][§5] |
| 62 | |
| 63 | </TableOfContents> |
| 64 | |
| 65 | [§1]: #technical-review |
| 66 | |
| 67 | <Heading |
| 68 | level='h2' |
| 69 | slug='technical-review' |
| 70 | >Un repaso técnico: ¿qué son los componentes de servidor?</Heading> |
| 71 | |
| 72 | El punto clave de RSC es que los componentes se dividen en dos categorías, |
| 73 | componentes de <b class='server'>"servidor"</b> y componentes de <b class='client'>"cliente"</b>. Los componentes de servidor pueden usar ni |
| 74 | `useState` ni `useEffect`, pero pueden hacer uso de funciones `async` y emplear |
| 75 | herramientas de backend, permitiendo por ejemplo llamar directamente a una base |
| 76 | de datos. Los componentes de cliente constituyen el modelo tradicional, en el |
| 77 | que hay código en el backend para generar HTML y en el frontend para administrar |
| 78 | el DOM usando `window.document.*`. |
| 79 | |
| 80 | > Ahí va el primer desastre: ¡la nomenclatura! React usa las palabras <b class='server'>"servidor"</b> y <b class='client'>"cliente"</b> para referirse a cosas muy específicas, ignorando sus definiciones existentes. No habría ningún problema, si no fuera por que ¡los componentes de <b class='client'>cliente</b> también pueden correr en el backend! En este artículo, utilizaré los términos <b>"backend"</b> y <b>"frontend"</b> para describir los ámbitos de ejecución en los que existen las aplicaciones web: un proceso de Node.js y un navegador web, respectivamente. |
| 81 | |
| 82 | Este modelo de componentes de <b class='server'>servidor</b> y de <b class='client'>cliente</b> es curioso. Dado que las funciones integradas, como |
| 83 | `<Suspense />`, se serializan a través de la red, la obtención de datos se puede |
| 84 | modelar de manera trivial con <b class='server'>componentes de servidor</b> |
| 85 | asíncronos, y la interfaz de usuario alternativa funciona como si fuera del lado |
| 86 | del cliente. |
| 87 | |
| 88 | ```tsx filename="src/app/[username]/page.tsx" tint="server" |
| 89 | // En este artículo, los componentes de servidor están resaltados en rojo. |
| 90 | export default async function Page({ params }) { |
| 91 | // Los parámetros de la página se pasan como una Promise resuelta. |
| 92 | const { username } = await params; |
| 93 | |
| 94 | // Los componentes `UserInfo` y `UserPostList` serán corridos a la vez. En |
| 95 | // cuanto `UserInfo` esté listo, el visitante verá la página con un |
| 96 | // `PostListSkeleton` si la lista de publicaciones no está lista. |
| 97 | return <main> |
| 98 | <UserInfo username={username} /> |
| 99 | |
| 100 | <Suspense fallback={<PostListSkeleton />}> |
| 101 | <UserPostList username={username} /> |
| 102 | </Suspense> |
| 103 | </main> |
| 104 | } |
| 105 | |
| 106 | // Evitamos waterfalls teniendo varios componentes evaluados a la vez. |
| 107 | |
| 108 | async function UserInfo({ username }) { |
| 109 | const user = await fetchUserInfo(username); |
| 110 | return <> |
| 111 | <h1>{user.displayName}</h1> |
| 112 | {user.bio ? <Markdown content={user.bio} /> : ""} |
| 113 | </> |
| 114 | } |
| 115 | |
| 116 | async function UserPostList({ username }) { |
| 117 | const posts = await fetchUserPostList(username); |
| 118 | return /* interfaz de usuario de la lista de publicaciones omitida por brevedad */; |
| 119 | } |
| 120 | ``` |
| 121 | |
| 122 | El ejemplo anterior no usa nada de JavaScript (sin contar el tamaño del empaquetado |
| 123 | _gzipped_ de 40kB del propio React) para la interfaz de usuario ni para la |
| 124 | obtención de datos &mdash; ¡sólo envía el HTML! Por ejemplo, el _parser_ (analizador) |
| 125 | imaginario de Markdown dentro del componente `<Markdown />` se queda en el |
| 126 | backend. En aquellos casos en los que se necesita un frontend interactivo, |
| 127 | simplemente se puede crear un <b class='client'>componente de cliente</b> |
| 128 | poniéndolo en un archivo empezando con `"use client"`. |
| 129 | |
| 130 | ```tsx filename="src/components/CopyButton.tsx" tint="client" |
| 131 | "use client"; // Este comentario marca el archivo para el empaquetado del lado del cliente. |
| 132 | |
| 133 | export function CopyButton({ url }) { |
| 134 | return <> |
| 135 | <span>{url}</span> |
| 136 | <button onClick={() => { |
| 137 | const full = new URL(url, location.href); |
| 138 | navigator.clipboard.writeText(full.href); |
| 139 | // omitiendo todo el manejo de errores, la interfaz de éxito, los estilos... |
| 140 | }}>copy</button> |
| 141 | </> |
| 142 | } |
| 143 | ``` |
| 144 | ```tsx filename="src/app/q+a/Card.tsx" tint="server" |
| 145 | export function Card() { |
| 146 | return <article> |
| 147 | <header> |
| 148 | {/* Hace que el navegador importe el botón de copiar */} |
| 149 | <CopyButton url="/q+a/2506010139" /> |
| 150 | </header> |
| 151 | <p> |
| 152 | {/* Procesa el Markdown en el backend */} |
| 153 | <Markdown content=".........." /> |
| 154 | </p> |
| 155 | </article> |
| 156 | } |
| 157 | ``` |
| 158 | |
| 159 | [§2]: #real-world-pitfalls |
| 160 | |
| 161 | <Heading |
| 162 | level='h2' |
| 163 | slug='real-world-pitfalls' |
| 164 | >Los inconvenientes del enrutador de aplicación</Heading> |
| 165 | |
| 166 | Tras dejar [Bun] como ingeniero de runtime (donde implementé el [empaquetado de |
| 167 | componentes de servidor][Server Components bundling] y [una plantilla de |
| 168 | RSC][bun-rsc]), me uní a una pequeña compañía, trabajando en la línea de fuego: |
| 169 | una aplicación de Next.js con un backend de Hono. Lo siguiente es una versión |
| 170 | simplificada de los problemas con los que me enfrenté tratando de mantener y |
| 171 | desarrollar características nuevas. Como resultado de todos ellos, perdimos |
| 172 | todos el tiempo intentando eludir fallos de diseño o explicándonos por qué algo |
| 173 | que no debería ser un problema para empezar se ha convertido en un obstáculo |
| 174 | inamovible. |
| 175 | |
| 176 | [Bun]: https://bun.com |
| 177 | [Server Components bundling]: https://github.com/oven-sh/bun/blob/67f0c3e016aa479738469adac2b79a1862b88122/src/bake/bake.d.ts |
| 178 | [bun-rsc]: https://github.com/oven-sh/bun/tree/e7790894d92b730758ecadf971cb935063508dfb/src/bake/bun-framework-react |
| 179 | |
| 180 | [§2.1]: #optimistic-updates |
| 181 | |
| 182 | <Heading |
| 183 | level='h3' |
| 184 | slug='optimistic-updates' |
| 185 | >Las actualizaciones optimistas son imposibles</Heading> |
| 186 | |
| 187 | La documentación de Next.js respecto a la mutación [no menciona las |
| 188 | actualizaciones optimistas][nextjs-updating-data], parece ser que no pensaron en |
| 189 | este caso. Los componentes renderizados por el <b class='server'>servidor de |
| 190 | React</b> no pueden ser modificados tras ser montados por diseño. Los elementos |
| 191 | que puedan tener cambios deben estar dentro de un componente de cliente, pero no |
| 192 | puede estos componentes no pueden dar lugar a obtención de datos, incluso |
| 193 | durante el renderizado del lado del servidor (SSR) en el backend. Esto deriva en |
| 194 | componentes de servidor incómodamente diminutos que sólo se encargan de obtener |
| 195 | ciertos datos y tienen un componente homólogo de cliente que contiene una |
| 196 | versión prácticamente estática de la página. |
| 197 | |
| 198 | ```tsx filename="src/app/user/[username]/page.tsx" tint="server" |
| 199 | |
| 200 | export default async function Page() { |
| 201 | const user = await fetchUserInfo(username); |
| 202 | return <ProfileLayout> |
| 203 | <UserProfile user={user} /> |
| 204 | </ProfileLayout>; |
| 205 | } |
| 206 | ``` |
| 207 | ```tsx filename="src/app/user/[username]/UserProfile.tsx" tint="client" |
| 208 | |
| 209 | "use client"; // Toca mover el código de cliente a otro archivo! |
| 210 | |
| 211 | export function UserProfile({ user: initialUser }) { |
| 212 | // Existen muchas librerías de gestión de estado excelentes; |
| 213 | // para simplificar, usaremos una celda de estado. |
| 214 | const [user, optimisticUpdateUser] = useState(initialUser); |
| 215 | |
| 216 | async function onEdit(newUser) { |
| 217 | optimisticUpdateUser(newUser); |
| 218 | const resp = await fetch("...", { |
| 219 | method: 'POST', |
| 220 | body: JSON.stringify(newUser), |
| 221 | ... // (encabezados, credenciales, trazado y más) |
| 222 | }) |
| 223 | if (!resp.ok) /* ¡recuerda siempre comprobar si se ha dado algún error! */ |
| 224 | } |
| 225 | |
| 226 | return <main>{/* interfaz de usuario con campos editables... */}</main>: |
| 227 | } |
| 228 | ``` |
| 229 | |
| 230 | A medida que más partes de la página necesitan interactividad, se vuelve más |
| 231 | lioso mantener las partes estáticas puramente del lado del servidor. En la |
| 232 | aplicación de trabajo, casi cada componente de la interfaz de usuario muestra |
| 233 | datos dinámicos. Un [`WebSocket`][ws] sincroniza los datos en tiempo real a |
| 234 | medida que se actualizan (por ejemplo, el estado en línea y el perfil básico de |
| 235 | una tarjeta de usuario). Como estas configuraciones de componentes son más |
| 236 | difíciles de entender y mantener para los ingenieros, casi todas nuestras |
| 237 | páginas están marcadas como `"use client"` con un `page.tsx` que define la |
| 238 | obtención de datos necesaria. |
| 239 | |
| 240 | Un ejemplo más concreto de cómo queda esto en la práctica con la librería de |
| 241 | obtención de datos que usamos en el trabajo, [TanStack Query]. |
| 242 | |
| 243 | [TanStack Query]: https://github.com/tanstack/query#readme |
| 244 | |
| 245 | ```ts filename="src/queries/users.ts" |
| 246 | // Tenemos una función auxiliar `defineQuery` para la seguridad de tipos en el trabajo. |
| 247 | // Los obtenedores de datos son triviales y pueden correr tanto en el backend como en el frontend. |
| 248 | export const queryUserInfo = (username) => ({ |
| 249 | queryKey: ['user', username], |
| 250 | queryFn: async ({ ... }) => /* obtener datos */ |
| 251 | }); |
| 252 | ``` |
| 253 | ```tsx filename="src/app/user/[username]/page.tsx" tint="server" |
| 254 | export default async function Page({ params }) { |
| 255 | const { username } = await params; |
| 256 | |
| 257 | // No hay estado global en el servidor de React. Como los layouts |
| 258 | // se ejecutan en paralelo, toca reconstruir el `QueryClient` de |
| 259 | // TanStack varias veces por ruta. |
| 260 | const queryClient = new QueryClient(); |
| 261 | await queryClient.ensureQueryData(queryUserInfo(username)); |
| 262 | |
| 263 | // HydrationBoundary es un componente de cliente que pasa datos |
| 264 | // JSON del servidor de React al componente de cliente. |
| 265 | return <HydrationBoundary state={dehydrate(queryClient)}> |
| 266 | <ClientPage /> |
| 267 | </HydrationBoundary>; |
| 268 | } |
| 269 | ``` |
| 270 | ```tsx filename="src/app/user/[username]/ClientPage.tsx" tint="client" |
| 271 | "use client"; |
| 272 | export function ClientPage() { |
| 273 | const { username } = useParams(); |
| 274 | const { data: user } = useSuspenseQuery(queryUserInfo(username)); |
| 275 | |
| 276 | // ... algunos hooks |
| 277 | |
| 278 | return <main> |
| 279 | {/* ... una página web interactiva */} |
| 280 | </main>; |
| 281 | } |
| 282 | ``` |
| 283 | |
| 284 | Este ejemplo necesita tres archivos por las reglas del empaquetado de los |
| 285 | componentes de servidor (el componente de cliente necesita `"use client"` y los |
| 286 | archivos de componentes de servidor no suelen poderse importar en el cliente |
| 287 | debido a las importaciones exclusivas de servidor). En el enrutador de páginas |
| 288 | (Pages Router), podría haberlo hecho todo en un solo archivo por el tree-shaking |
| 289 | que poseen `getStaticProps` y `getServerSideProps`. |
| 290 | |
| 291 | [ws]: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API |
| 292 | [nextjs-updating-data]: https://nextjs.org/docs/app/getting-started/updating-data |
| 293 | |
| 294 | |
| 295 | [§2.2]: #redundant-fetches |
| 296 | |
| 297 | <Heading |
| 298 | level='h3' |
| 299 | slug='redundant-fetches' |
| 300 | >Cada navegación supone una petición</Heading> |
| 301 | |
| 302 | Como el enrutador de aplicación inicia cada página como componente de servidor, |
| 303 | con áreas pequeñas de interactividad (idealmente), navegar a una nueva página |
| 304 | ¡*fuerza* una petición al servidor de Next.js, independientemente de los datos |
| 305 | que el cliente ya tenga disponibles! Incluso con un archivo `loading.tsx`, al |
| 306 | abrir `/`, navegar a `/other/ y luego volver a `/`, se mostrará el estado de |
| 307 | carga mientras vuelve a obtener los datos de la página de inicio. |
| 308 | |
| 309 | Para lo único para lo que esto funciona bien es para el **contenido |
| 310 | perfectamente estático**, donde las navegaciones instantáneas y la precarga |
| 311 | (prefetching) funcionan genial. Pero **las aplicaciones web no son estáticas**, |
| 312 | tienen mucho contenido dinámico. Haber iniciado sesión afecta a la página de |
| 313 | inicio, cosa que es irritante porque el cliente literalmente ya tiene todo lo |
| 314 | que necesita para mostrar la página instantáneamente. Ni siquiera han cambiado |
| 315 | las cookies. |
| 316 | |
| 317 | > **nota**: Probándolo más en un proyecto en blanco, he observado casos en los |
| 318 | > que el código de frontend de Next precarga rutas **sin ningún contenido |
| 319 | > real**. En el ejemplo de hello world, era una carga de 1.8kB de RSC que |
| 320 | > apuntaba a 2 fragmentos de JS diferentes 4 veces distintas. Esto es un desperdicio |
| 321 | > puro de nuestro ancho de banda y egreso, especialmente considerando que toda |
| 322 | > esta información se vuelve a obtener cuando de verdad hago clic en el enlace. |
| 323 | > |
| 324 | > ```json whitespace="pre-wrap" |
| 325 | > 1:"$Sreact.fragment" |
| 326 | > 2:I[39756,["/_next/static/chunks/ff1a16fafef87110.js","/_next/static/chunks/7dd66bdf8a7e5707.js"],"default"] |
| 327 | > 3:I[37457,["/_next/static/chunks/ff1a16fafef87110.js","/_next/static/chunks/7dd66bdf8a7e5707.js"],"default"] |
| 328 | > 4:I[97367,["/_next/static/chunks/ff1a16fafef87110.js","/_next/static/chunks/7dd66bdf8a7e5707.js"],"ViewportBoundary"] |
| 329 | > 6:I[97367,["/_next/static/chunks/ff1a16fafef87110.js","/_next/static/chunks/7dd66bdf8a7e5707.js"],"MetadataBoundary"] |
| 330 | > 7:"$Sreact.suspense" |
| 331 | > 0:{"b":"TdwnOXsfOJapNex_HjHGt","f":[["children","other",["other",{"children":["__PAGE__",{}]}],["other",["$","$1","c",{"children":[null,["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],{"children":null},[["$","div","l",{"children":"loading..."}],[],[]],false],["$","$1","h",{"children":[null,["$","$1","KCFxAJdIDH3BlYXAHsbcVv",{"children":[["$","$L4",null,{"children":"$L5"}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],["$","$L6","KCFxAJdIDH3BlYXAHsbcVm",{"children":["$","div",null,{"hidden":true,"children":["$","$7",null,{"fallback":null,"children":"$L8"}]}]}]]}],false]],"S":false} |
| 332 | > 5:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]] |
| 333 | > 9:I[27201,["/_next/static/chunks/ff1a16fafef87110.js","/_next/static/chunks/7dd66bdf8a7e5707.js"],"IconMark"] |
| 334 | > 8:[["$","title","0",{"children":"Create Next App"}],["$","meta","1",{"name":"description","content":"Generated by create next app"}],["$","link","2",{"rel":"icon","href":"/favicon.ico?favicon.0b3bf435.ico","sizes":"256x256","type":"image/x-icon"}],["$","$L9","3",{}]] |
| 335 | > ``` |
| 336 | > |
| 337 | > Revisándolo, me di cuenta de que en realidad hay algo de contenido: el estado |
| 338 | > de carga. ¿Lo ves? |
| 339 | > |
| 340 | > ```json |
| 341 | > ["$","div","l",{"children":"loading..."}] |
| 342 | > ``` |
| 343 | > |
| 344 | > Sigue siendo un desperdicio grande, ya que todos estos datos se vuelven a |
| 345 | > emitir en el RSC de la propia página. |
| 346 | |
| 347 | Parece ser que la solución a esto es [`staleTime`][nextjs-stale], pero está |
| 348 | marcado como experimental y "no recomendado para producción". Es una vergüenza |
| 349 | el hecho de que esto sea una opción que no está por defecto y que parece haber |
| 350 | sido una idea de último momento. Incluso usándola, no es posible hacer que |
| 351 | varias páginas que hacen referencia a los mismos datos los compartan. |
| 352 | |
| 353 | Un ejemplo de un estado de carga irrepresentable con el enrutador de aplicación |
| 354 | es el de ciertas páginas, como una página de propuestas (issues) en un proyecto |
| 355 | de git, al hacer clic en un nombre de usuario para ir a su página de perfil. Con |
| 356 | `loading.tsx`, la página entera es un esqueleto, pero al modelar estas consultas |
| 357 | con TanStack Query es posible mostrar el nombre de usuario y avatar |
| 358 | instantáneamente mientras cargan la biografía del usuario y sus repositorios. |
| 359 | Los componentes de servidor son incompatibles con esta tipo de navegación porque |
| 360 | los datos sólo están disponibles en componentes renderizados, con lo cual deben |
| 361 | ser obtenidos una vez más. |
| 362 | |
| 363 | [nextjs-stale]: https://nextjs.org/docs/app/api-reference/config/next-config-js/staleTimes |
| 364 | [nextjs-stale-bug]: https://github.com/vercel/next.js/issues/70661 |
| 365 | |
| 366 | En nuestro sitio Next.js, tenemos esta línea de código en nuestros obtenedores |
| 367 | de datos en los componentes de servidor para hacer que las navegaciones suaves |
| 368 | vayan más rápido, saltándose la fase de obtención de datos por completo. |
| 369 | |
| 370 | ```tsx filename="src/util/tanstack-query-helpers.server.ts" tint="server" |
| 371 | export function serverSidePrefetchQueries(queries) { |
| 372 | if ((await headers()).get("next-url")) { |
| 373 | // Se trata de una navegación suave. Nos SALTAMOS la precarga para |
| 374 | 	// aumentar la velocidad. Puede que el cliente ya tenga estos datos y, |
| 375 | 	// de lo contrario, tienen el estado de carga. Idealmente, no existiría |
| 376 | 	// esta petición -- el lado de cliente ya tiene casi TODO el código ya |
| 377 | 	// que la aplicación está escrita principalmente usando componentes de |
| 378 | 	// cliente. Un fallo de diseño de parte del enrutador de aplicación, la |
| 379 | 	// verdad. |
| 380 | return; |
| 381 | } |
| 382 | // ... lógica de precarga de datos ... |
| 383 | } |
| 384 | ``` |
| 385 | |
| 386 | Aparte, `loading.tsx` debería contener las llamadas a `useQuery` para que, |
| 387 | mientras se realiza la petición para el RSC vacío, se obtengan los datos si de |
| 388 | verdad se necesitan. De hecho, el estado `loading.tsx` puede ser directamente el |
| 389 | componente de cliente y se mostrará la página de cliente. |
| 390 | |
| 391 | ```tsx filename="src/app/user/[username]/loading.tsx" tint="client" |
| 392 | "use client"; |
| 393 | export default function PageLoadingSkeleton() { |
| 394 | return <ClientPage />; |
| 395 | } |
| 396 | ``` |
| 397 | |
| 398 | > En el trabajo, simplemente hacemos que nuestros archivos `loading.tsx` |
| 399 | > contengan las llamadas a `useQuery` y muestren un esqueleto. Esto se debe a |
| 400 | > que cuando Next.js carga el componente de servidor en sí, remonta la página |
| 401 | > entera sí o sí. No se da ningún tipo de VDOM diffing, así que todos los hooks |
| 402 | > (`useState`) se reiniciarán un poco después de que la petición se complete. |
| 403 | > Intenté reproducir un caso simple en el que estaba *suplicándole* a Next.js |
| 404 | > que tan solo *actualizara el DOM existente* y preservara el estado, pero |
| 405 | > directamente no lo hace. Por suerte, el tiempo que tarda la llamada RSC en |
| 406 | > blanco es suficientemente corto. |
| 407 | |
| 408 | [§2.3]: #layout-restrictions |
| 409 | |
| 410 | <Heading |
| 411 | level='h3' |
| 412 | slug='layout-restrictions' |
| 413 | >Los layouts tienen restricciones artificiales</Heading> |
| 414 | |
| 415 | Los layouts pueden obtener datos, pero no pueden observar o alterar la petición |
| 416 | de ninguna manera. Está hecho así para que Next.js pueda obtener y cachear los |
| 417 | layouts cada vez que quiera. En cualquier otro framework, los layouts son |
| 418 | componentes normales sin diferencia de otros componentes en la página. |
| 419 | |
| 420 | Obtener los layouts por sí mismos es una idea chula, pero acaba siendo estúpida, |
| 421 | pues toda obtención de datos debe rehacerse para cada layout. No puedes |
| 422 | compartir un `QueryClient`, en su lugar debes usar su |
| 423 | [`fetch` _monkey-patched_][nextjs-fetch] para cachear la misma petición `GET` |
| 424 | tal y como prometen. |
| 425 | |
| 426 | Cuando me pregunta un compañero de trabajo por qué Next.js rechaza código, ya ni |
| 427 | intento explicar los entresijos, directamente digo *"Es un problema de habilidad |
| 428 | de Next.js, no te preocupes, que le voy a dar fuego pronto"*. Estas reglas son |
| 429 | demasiado difíciles para que un desarrollador promedio las entienda. |
| 430 | |
| 431 | [nextjs-fetch]: https://nextjs.org/docs/app/api-reference/functions/fetch |
| 432 | |
| 433 | [§2.4]: #rsc-payload |
| 434 | |
| 435 | <Heading |
| 436 | level='h3' |
| 437 | slug='rsc-payload' |
| 438 | >Te llevas todo el contenido dos veces de todas formas</Heading> |
| 439 | |
| 440 | A diferencia de la ["arquitectura de islas"][islands], los componentes de |
| 441 | servidor deben ser hidratados en el frontend para poder emplear `Suspense` y |
| 442 | mantener el estado de los componentes de cliente. Al hacer navegaciones suaves, |
| 443 | se obtiene mediante `fetch` la "carga de RSC" (que no es HTML en absoluto). |
| 444 | Al hacer una recarga fresca de la página, se necesita HTML para |
| 445 | [pintar por primera vez][first paint], pero la información sobre los componentes |
| 446 | de cliente y `Suspense` no está en dicho HTML. La solución de React es **enviar |
| 447 | una segunda copia del _markup_ de la página entera**. Un ejemplo de lo que |
| 448 | enviaría un servidor de producción de Next.js en un renderizado dinámico de una |
| 449 | página sería algo así: |
| 450 | |
| 451 | [first paint]: https://web.dev/articles/fcp |
| 452 | |
| 453 | ```html filename="GET /user/clover" |
| 454 | <!DOCTYPE html> |
| 455 | <html> |
| 456 | <head> |
| 457 | {etiquetas link y meta} |
| 458 | </head> |
| 459 | <body> |
| 460 | {renderizado del lado del servidor} |
| 461 | <script> |
| 462 | // un script de inicialización que inicia `__next_f` |
| 463 | // como un arreglo. en cuanto carga React, la función |
| 464 | // `.push` se modifica para escribir nuevos fragmentos |
| 465 | // al decodificador de RSC directamente. tiene algunos |
| 466 | // auxiliares de DOM también |
| 467 | (self.__next_f=self.__next_f||[]).push([0]) |
| 468 | </script> |
| 469 | <script> |
| 470 | // la carga de RSC para el shell de la aplicación. |
| 471 | self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[658993,[\"/_next/st{...}"]) |
| 472 | </script> |
| 473 | |
| 474 | <!-- |
| 475 | NO está escrita todavía la etiqueta </body>, ya que hay una |
| 476 | frontera de Suspense sin resolver. con el paso del tiempo, |
| 477 | se escriben más datos. |
| 478 | --> |
| 479 | <div class="user-post-list"> |
| 480 | {renderizado del lado del servidor de una frontera de Suspense} |
| 481 | </div> |
| 482 | <script> |
| 483 | // la carga de RSC para la frontera de Suspense |
| 484 | self.__next_f.push([2,"14:[\"$\",\"div\",null,{\"children\":[[\"$\",\"h4\"{...}"]) |
| 485 | </script> |
| 486 | |
| 487 | <!-- se repiten etiquetas HTML y script hasta que la página acaba --> |
| 488 | </body> |
| 489 | </html> |
| 490 | ``` |
| 491 | |
| 492 | Esta solución **duplica el tamaño de la carga inicial de HTML**. Aún peor, la |
| 493 | carga de RSC incluye JSON en cadenas de texto de JS, un formato mucho menos |
| 494 | eficiente que HTML. Aunque parece comprimirse bien con brotli y renderizarse |
| 495 | rápido en el navegador, esto es un desperdicio. Con el patrón de hidratación, |
| 496 | los datos se podrían reutilizar localmente, por lo menos, para la interactividad |
| 497 | y otras páginas. |
| 498 | |
| 499 | Incluso en páginas con ínfima interactividad, pagas el precio. Como ejemplo, la |
| 500 | documentación de Next.js, al cargar su [página de |
| 501 | inicio](https://nextjs.org/docs), carga una página de alrededor de 750kB (250kB |
| 502 | de HTML y 500kB de etiquetas script), y el contenido aparece dos veces. |
| 503 | |
| 504 | Puedes comprobarlo pulsando <kbd>Cmd</kbd> + <kbd>Opt</kbd> + <kbd>u</kbd> en |
| 505 | Mac o <kbd>Ctrl</kbd> + <kbd>u</kbd> en otras plataformas y después |
| 506 | <kbd>Cmd</kbd> / <kbd>Ctrl</kbd> + <kbd>f</kbd> para ubicar cualquier cadena |
| 507 | en el blog, como "construyendo aplicaciones web full-stack". Aparece dos veces. |
| 508 | Y **es inevitable**, ya que es una parte fundamental de los componentes de |
| 509 | servidor de React. |
| 510 | |
| 511 | El formato de RSC definitivamente tiene más fallas, pero no tengo ganas de |
| 512 | ponerme a investigar por qué la cadena `/_next/static/chunks/6192a3719cda7dcc.js` |
| 513 | aparece 27 veces. ¿Qué diablos? ¿¿¿Dais el ancho de banda por gratis??? |
| 514 | |
| 515 | [islands]: https://www.patterns.dev/vanilla/islands-architecture/ |
| 516 | |
| 517 | [§2.5]: #turbopack |
| 518 | |
| 519 | <Heading |
| 520 | level='h3' |
| 521 | slug='turbopack' |
| 522 | >Turbopack da asco</Heading> |
| 523 | |
| 524 | Esta sección no es constructiva. |
| 525 | |
| 526 | - Turbopack no es ágil |
| 527 | - Turbopack emite código difícil de depurar (en modo de desarrollo) |
| 528 | - Turbopack produce mensajes de error malos en muchos casos |
| 529 | |
| 530 | Normalmente no le habría dado una sección en el blog a este punto, pero quiero |
| 531 | mostrar tres ejemplos de verdad, directamente extraídos del proyecto. |
| 532 | |
| 533 | El primero es un lugar donde, al refactorizar código para satisfacer los modelos |
| 534 | de componente de servidor/cliente, hice asíncrono un componente de cliente. Era |
| 535 | irritante porque no indicaba dónde estaba el error, sólo contenía el stack trace |
| 536 | del <b class='server'>servidor</b>. |
| 537 | |
| 538 |  |
| 539 | |
| 540 | Otro ejemplo de un error horrible: |
| 541 | |
| 542 |  |
| 543 | |
| 544 | > Tras arreglar el problema detrás de este segundo error (que ni siquiera recuerdo), |
| 545 | > el servidor de desarrollo se quedó colgado y tuve que reiniciarlo para que se recuperara. |
| 546 | |
| 547 | El último caso es el millón de veces que he puesto un _breakpoint_ (punto de |
| 548 | interrupción) en el depurador y la variable `hola` se convierte en |
| 549 | `__TURBOPACK__imported__module__$5b$project$5d2f$client$2f$src$2f$utils$2f$filename$2e$ts__$5b$app$2d$client$5d$__$28$ecmascript$29$__["hola"]` |
| 550 | y más mierda. |
| 551 | |
| 552 | Vale. Todo esto da asco. ¿Qué podemos hacer? |
| 553 | |
| 554 | [§3]: #ditching-nextjs |
| 555 | |
| 556 | <Heading |
| 557 | level='h2' |
| 558 | slug='ditching-nextjs' |
| 559 | >Dejando atrás Next.js y Vercel en el trabajo</Heading> |
| 560 | |
| 561 | Hay dos tipos de proyectos web: |
| 562 | |
| 563 | - Una página web con contenido mayoritariamente estático. |
| 564 | - Una aplicación web con componentes mayormente dinámicos e interactivos. |
| 565 | |
| 566 | Y Next.js es la herramienta equivocada para ambos. Si quieres una página web |
| 567 | estática, usa [Astro] o [Fresh]. Para quienes necesiten la potencia de React, |
| 568 | esta sección trata cómo pasé de estar atado a Next a [TanStack Start], de forma |
| 569 | incremental y sin baches. |
| 570 | |
| 571 | [Astro]: https://astro.build/ |
| 572 | [Fresh]: https://fresh.deno.dev/ |
| 573 | [TanStack Start]: https://tanstack.com/start/latest |
| 574 | |
| 575 | Todo empezó con esta configuración de Vite. |
| 576 | |
| 577 | ```ts filename="vite.config.ts" |
| 578 | const config = defineConfig(({ mode }) => { |
| 579 | const env = loadEnv(mode, process.cwd(), "NEXT_PUBLIC_"); |
| 580 | return { |
| 581 | // Usa el puerto predeterminado de Next.js, 3000 |
| 582 | server: { port: 3000 }, |
| 583 | // Usa el prefijo de variables de entorno predeterminado de Next.js, "NEXT_PUBLIC_" |
| 584 | define: Object.fromEntries(Object.entries(env).map( |
| 585 | ([k, v]) => [`process.env.${k}`, JSON.stringify(v)])), |
| 586 | plugins: [ |
| 587 | viteTsConfigPaths({ projects: ["./tsconfig.json"] }), |
| 588 | tailwindcss(), |
| 589 | // Para que mis compañeros de trabajo lo entendieran bien, empecé |
| 590 | // a portar las rutas en `src/tanstack-routes`; en cuanto acabara, |
| 591 | // volvería a cambiarlo a `src/routes`, como estaba por defecto. |
| 592 | tanstackStart({ |
| 593 | router: { routesDirectory: "src/tanstack-routes" }, |
| 594 | }), |
| 595 | viteReact(), |
| 596 | ], |
| 597 | resolve: { |
| 598 | // La clave para la migración incremental: redirigir `next` a otro lugar |
| 599 | alias: { next: path.resolve("./src/tanstack-next/") }, |
| 600 | conditions: ["tanstack"], |
| 601 | extensions: [ |
| 602 | // Permitir que un archivo como `utils/session.tanstack.ts` |
| 603 | // sobreescriba a `utils/session.ts` al ser importado. |
| 604 | ".tanstack.tsx", ".tanstack.ts", |
| 605 | // Extensiones de importación predeterminadas |
| 606 | ".mjs", ".js", ".mts", ".ts", |
| 607 | ".jsx", ".tsx", ".json", |
| 608 | ], |
| 609 | }, |
| 610 | }; |
| 611 | }); |
| 612 | ``` |
| 613 | |
| 614 | Después, me puse a buscar cada uso de una API de Next.js, para o eliminarlo o |
| 615 | crear un talón (_stub_) para TanStack. Por ejemplo, `src/tanstack-next/link.tsx` |
| 616 | implementa `next/link`: |
| 617 | |
| 618 | ```tsx filename="src/tanstack-next/link.tsx" |
| 619 | import { Link } from "@tanstack/react-router"; |
| 620 | import type { LinkProps } from "next/link"; |
| 621 | |
| 622 | export default function LinkAdapter({ href, ...rest }: LinkProps) { |
| 623 | return <Link {...rest} to={href as unknown as any} />; |
| 624 | } |
| 625 | ``` |
| 626 | |
| 627 | > Algunos de estos talones pueden ser extremadamente simples. Al principio, mi |
| 628 | > implementación de `useRouter` era `return {}`. Más tarde, ya tuve que añadir |
| 629 | > algunos métodos al objeto. El código no tiene por qué ser limpio, ya que es |
| 630 | > temporal. |
| 631 | |
| 632 | De ahí, el sitio nuevo puede importar casi cada componente de cliente o creando |
| 633 | talones para las APIs de Next.js que necesite o usando la extensión `.tanstack.ts` |
| 634 | para reimplementar la lógica archivo por archivo. Poco después, conseguí que |
| 635 | la página principal del sitio funcionara en TanStack Start e hicimos _merge_. |
| 636 | |
| 637 |  |
| 638 | |
| 639 | > En esta primera PR (solicitud de incorporación de cambios) sólo funcionaba una |
| 640 | > de nuestras páginas, y conseguí que funcionara con unas mil líneas de código |
| 641 | > añadido y 40 líneas eliminadas. Algunos parches previos eliminaban los pocos |
| 642 | > usos de `next/image` y `next/font`. |
| 643 | |
| 644 | Lo que quedaba era portar el resto de rutas. Lo único que perdemos al migrar de |
| 645 | Next.js a cualquier otro framework es el poder usar `await` con funciones de |
| 646 | obtención de datos en la interfaz de usuario. En la práctica, mover cada ruta |
| 647 | a una función `loader` esclareció qué era lo que pasaba al renderizar cada |
| 648 | página desde el servidor (SSR). Para páginas con varias llamadas de obtención de |
| 649 | datos, estas podían combinarse en una llamada API especial que devolviera todos |
| 650 | los datos relevantes a ellas. |
| 651 | |
| 652 | Para reiterar, en negrita: <strong style='color:var(--secondary)'>El camino de |
| 653 | migración de componentes de servidor es simplificar tu código &mdash; RSC |
| 654 | intrínsecamente te lleva por un camino caótico repleto de cosas |
| 655 | innecesarias</strong>. Casi todas las partes complejas de nuestro sitio se |
| 656 | volvieron más fáciles de entender para todos nuestros ingenieros. La única |
| 657 | excepción fue acostumbrar a todos a las nuevas convenciones para el enrutado |
| 658 | del sistema de archivos. Con suficientes ejemplos, todos lo acabamos pillando. |
| 659 | |
| 660 | Con la migración incremental ya operativa, no se rompía el _deployment_ existente |
| 661 | al añadir código nuevo. TanStack fue apoderándose del código y, con el tiempo, |
| 662 | fuimos eliminando todos los talones de Next.js y ganamos todas las maravillosas |
| 663 | [características de seguridad de tipos][type-safety features] que ofrece el |
| 664 | enrutador de TanStack (TanStack Router). Al final, el sitio rendía más rápido en |
| 665 | todos los aspectos: el modo de desarrollo, tiempos de carga en producción, |
| 666 | navegaciones suaves, y todo a un menor coste que nuestro _deployment_ de Next con |
| 667 | Vercel. |
| 668 | |
| 669 | [type-safety features]: https://tanstack.com/router/v1/docs/framework/react/guide/type-safety |
| 670 | |
| 671 | No somos los únicos sintiendo el cambio. Aunque intento evitar las redes |
| 672 | sociales, alguien me envió [los resultados del trabajo de Brian Anglin en |
| 673 | Superwall][superwall-twitter], mostrando reducciones de CPU increíbles usando |
| 674 | TanStack Start. También recuerdo el cambio de ChatGPT de Next.js a Remix hace un |
| 675 | año (conversaciones relevantes: [[1][chatgpt-1]] [[2][chatgpt-2]] [[3][chatgpt-3]]). |
| 676 | |
| 677 | [superwall-twitter]: https://twitter.com/BriansAngles/status/1978834116079436242#m |
| 678 | [chatgpt-1]: https://xcancel.com/ryanflorence/status/1831379475654947233 |
| 679 | [chatgpt-2]: https://old.reddit.com/r/reactjs/comments/1f97zgr/chatgpt_migrates_from_nextjs_to_remix |
| 680 | [chatgpt-3]: https://old.reddit.com/r/nextjs/comments/1f92jdv/chatgptcom_switched_from_nextjs_to_remix |
| 681 | |
| 682 | [§3.1]: #next-metadata |
| 683 | |
| 684 | <Heading |
| 685 | level='h3' |
| 686 | slug='next-metadata' |
| 687 | ><code>next/metadata</code> es maravilloso</Heading> |
| 688 | |
| 689 | Bajo mi punto de vista, esta es una de las pocas APIs buenas que tiene Next.js, |
| 690 | y fue el único lugar en nuestro código en el que el cambio a TanStack resultó en |
| 691 | mayor dificultad. En vez de empeorar el código, porté su API de metadatos a una |
| 692 | función normal y corriente, para que todos la pudieran usar. Solía tener un |
| 693 | puerto 1:1 en NPM, pero a comienzos del año simplifiqué su API a un archivo |
| 694 | pequeño y comprensible. En el momento de escribir esta publicación, he añadido |
| 695 | una API `meta.toTags` compatible con TanStack que puede instalarse desde |
| 696 | [JSR][lib-jsr] o [NPM][lib-npm], o simplemente la puedes copiar a tu proyecto. |
| 697 | |
| 698 | > **aviso**: Por limitaciones de tiempo para escribir este artículo, la librería |
| 699 | > todavía no está actualizada. Probablemente lo haré ~~para el final de esta |
| 700 | > semana (24 de octubre)~~ pronto... Por ahora, puedo compartir la versión que |
| 701 | > uso en el trabajo: |
| 702 | > [`meta.tanstack.ts`](https://paperclover.net/file/2025/blog-everyone-hates-nextjs/meta.tanstack.ts). |
| 703 | |
| 704 | ```tsx |
| 705 | // una vez en tu proyecto |
| 706 | import * as meta from "@clo/lib/meta.ts"; |
| 707 | |
| 708 | export const defineHead = meta.toTags.bind(null, { |
| 709 | // opciones para todo el sitio |
| 710 | base: new URL("https://paperclover.net"), |
| 711 | titleTemplate: (title) => [title, "paper clover"] |
| 712 | .filter(Boolean).join(' | '), |
| 713 | // ... |
| 714 | }); |
| 715 | |
| 716 | // para cada página... |
| 717 | export const Route = createFileRoute("/blog")({ |
| 718 | head: () => |
| 719 | defineHead({ |
| 720 | title: "blog de clover", // usando la plantilla `titleTemplate` |
| 721 | description: "una gatita maúlla sobre sus opiniones tecnológicas", |
| 722 | canonical: "/blog", // yuxtapuesto a `base` |
| 723 | |
| 724 | // Cuando se especifica, configura el embed (embebido) |
| 725 | // de Open Graph y Twitter, usando el título y la |
| 726 | // descripción de la página por defecto. |
| 727 | // La configuración predeterminada está bien, |
| 728 | // pero se pueden especificar más opciones. |
| 729 | embed: { |
| 730 | image: "/img/blog.webp", |
| 731 | }, |
| 732 | |
| 733 | // Todas las etiquetas meta exóticas se hacen con un fragmento |
| 734 | // JSX. No se renderiza React, sólo se itera sobre las etiquetas. |
| 735 | // Mi objetivo era cubrir el 99% de los casos comunes. |
| 736 | extra: <> |
| 737 | <meta name="site-verification" content="waffles" />, |
| 738 | </>, |
| 739 | }), |
| 740 | |
| 741 | component: Page, |
| 742 | }); |
| 743 | |
| 744 | function Page() { |
| 745 | ... |
| 746 | } |
| 747 | ``` |
| 748 | |
| 749 | Mi versión no se preocupaba con cubrir todas las posibilidades del objeto de |
| 750 | metadatos de Next.js; usa JSX en línea (_inline_) para llenar ese hueco. |
| 751 | |
| 752 | [lib-jsr]: https://jsr.io/@clo/lib |
| 753 | [lib-npm]: https://npmjs.com/@paperclover/lib |
| 754 | |
| 755 | [§3.2]: #vercel-og |
| 756 | |
| 757 | <Heading |
| 758 | level='h3' |
| 759 | slug='ditching-nextjs' |
| 760 | ><code>next/og</code> está bien también</Heading> |
| 761 | |
| 762 | No tengo una opinión fuerte al respecto. Sólo quiero recordar a todos que existe |
| 763 | el paquete `@vercel/og`. |
| 764 | |
| 765 | [§4]: #experience-feels-like-the-usual |
| 766 | |
| 767 | <Heading |
| 768 | level='h2' |
| 769 | slug='experience-feels-like-the-usual' |
| 770 | >Mi experiencia parece ser la típica</Heading> |
| 771 | |
| 772 | En la Next.js Conf 2024, todos hablaban maravillas de los componentes de |
| 773 | servidor. No recuerdo exactamente con quién fue que hablé, pero todos los |
| 774 | grandes nombres estaban completamente a favor de ellos. Habiendo implementado el |
| 775 | empaquetador de RSC, yo vi algunos de los problemas en el formato. Ahora, viendo |
| 776 | que Next 15 "estabilizó" el enrutador de aplicaciones el año pasado, muchas |
| 777 | compañías están construyendo productos con él, dándose cuenta de estos |
| 778 | inconvenientes de primera mano. |
| 779 | |
| 780 | Llegué tarde al mundo de Next.js, pues empecé en junio con la versión 15. Pero |
| 781 | todos con los que he hablado están de acuerdo con mis observaciones. Todos con |
| 782 | los que hablé en la 1.3 Party de Bun estaban de acuerdo conmigo. Incluso gente |
| 783 | de Vercel me ha dicho que no les gusta cómo es usar Next.js. |
| 784 | |
| 785 | Espero que, al estabilizarse TanStack Start, se vuelva el nuevo sustituto de |
| 786 | Next.js que todos quieran. |
| 787 | |
| 788 | [§5]: #prefer-respectful-tools |
| 789 | |
| 790 | <Heading |
| 791 | level='h2' |
| 792 | slug='prefer-respectful-tools' |
| 793 | >Opta por herramientas que te respeten</Heading> |
| 794 | |
| 795 | El ecosistema de JavaScript es un desastre y ese desastre es por el que la gente |
| 796 | se burla del desarrollo web. Muchas veces he pensado que el desastre de trabajar |
| 797 | en la web no tenía solución, pero el desastre eran en verdad las librerías |
| 798 | comúnmente usadas de las que me rodeaba. Al quitar esa capa, las tecnologías de |
| 799 | desarrollo web modernas son maravillosas. |
| 800 | |
| 801 | Llevo desarrollando esta página web desde cero, sin framework, desde finales de |
| 802 | 2024, implementando sistemas como mi propio [widget de progreso para |
| 803 | aplicaciones de terminal][progress], [proxy para archivos |
| 804 | estáticos][file-cache], sistema de compilación incremental, y muchos más |
| 805 | componentes. Trabajar en este código ha llevado a las mejores sesiones de código |
| 806 | (en términos de felicidad) en años. Los visitantes de *[paper clover]* se llevan |
| 807 | una página web de mejor calidad; las minilibrerías que he creado las [extraigo |
| 808 | para uso público][lib], todos salen ganando. |
| 809 | |
| 810 | Este nivel de "desde cero" es demasiado para la mayoría, especialmente en el |
| 811 | trabajo. Diría que, como mínimo, deberíamos dedicar atención y dinero sólo a |
| 812 | herramientas de alta calidad que nos respeten. Y Next.js y la empresa detrás de |
| 813 | ello, Vercel, no caen dentro de ese criterio. |
| 814 | |
| 815 | Si usas Next.js y sientes que tu experiencia tampoco te recuerda al respeto, |
| 816 | reflexiona si tus compañeros de trabajo y tú queréis seguir apoyando su [imperio |
| 817 | serverless][serverless empire]. El ecosistema de Vite parece bastante decente |
| 818 | para desarrollar ahora mismo, pero no tengo mucha experiencia todavía en usar |
| 819 | sus herramientas a gran escala en producción. El [lanzamiento de Vite+ de |
| 820 | Void0][vite-plus] parece interesante, pero sólo el tiempo dirá si estas |
| 821 | herramientas, financiadas por capital, nos respetarán (a los usuarios y a los |
| 822 | desarrolladores) a largo plazo. |
| 823 | |
| 824 | La Next.js Conf 2025, en el momento de escribir, es [mañana][next-conf]. En vez |
| 825 | de comprarme una entrada de $800, decidí destinar ese dinero al [equipo de |
| 826 | TanStack][tanstack-donate] por [respetar y mejorar el ecosistema de desarrollo |
| 827 | web][tanstack-ethos]. |
| 828 | |
| 829 | [paper clover]: https://paperclover.net/ |
| 830 | [lib]: https://git.paperclover.net/clo/sitegen/src/branch/master/lib#readme |
| 831 | [progress]: https://git.paperclover.net/clo/sitegen/src/branch/master/lib/progress.ts |
| 832 | [file-cache]: https://git.paperclover.net/clo/sitegen/src/branch/master/src/file-viewer/cache.ts |
| 833 | |
| 834 | [next-conf]: https://nextjs.org/conf |
| 835 | [vite-plus]: https://viteplus.dev/ |
| 836 | [tanstack-ethos]: https://tanstack.com/ethos |
| 837 | [tanstack-donate]: https://github.com/sponsors/tannerlinsley |
| 838 | [serverless empire]: https://youtu.be/SCIfWhAheVw |
| 839 | |
| 840 | ## Lo que aguarda el futuro |
| 841 | |
| 842 | Paulatinamente, he estado reemplazando mucho software que me falta al respeto |
| 843 | con mejores alternativas. Algunos ejemplos son GitHub, Visual Studio Code, |
| 844 | DaVinci Resolve, Discord, Google Drive/Workspace, entre muchos otros. Pienso |
| 845 | escribir más en este blog sobre mis proyectos técnicos (la librería de progreso, |
| 846 | el propósito de mi generador de sitio propio, aprendizajes de mi trabajo |
| 847 | actual), incluyendo algunos de mis proyectos pasados en Bun (detalles sobre HMR, |
| 848 | el sistema de reporte de errores y la locura de sistema para empaquetar módulos |
| 849 | integrados). Si te interesa, por favor suscríbete a la lista de correos: |
| 850 | |
| 851 | <a href="mailto:subscribe@paperclover.net?subject=paper%20clover%20mailing%20list&body=I%20would%20like%20to%20be%20subscribed%20to%20the%20following%20mailing%20lists%3A%0A%0A-%20Technical%20Blog%20Posts%20-%20YES%0A-%20Art%20(Original%20Music%2FVideo)%20-%20YES%0A%0A(feel%20free%20to%20write%20whatever%20else%20you%20want)">haz clic aquí para enviar un correo a <code>subscribe@paperclover.net</code>, pidiendo ser añadido a la lista de correos.</a> (llevo esta lista de correos a mano) |
| 852 | |
| 853 | [volver al inicio](#top) &mdash; [pregúntame algo sobre este artículo](/q+a) |
| 854 | |
| 855 | <br /> |
| 856 | <br /> |
| 857 | <br /> |
| 858 | <br /> |
| 859 | <br /> |
| 860 | <br /> |
| 861 | <footer> |
| 862 | 2025 (c) paper clover |
| 863 | </footer> |
| 864 | |
| 865 | </Layout> |
| 866 | |
| 867 | <br /> |
| 868 | |