> ## Documentation Index
> Fetch the complete documentation index at: https://blog.mapptech.com.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Renderización Web

> Las estrategias de renderizado en el desarrollo web han evolucionado, ofreciendo diferentes compensaciones entre rendimiento, SEO, interactividad y complejidad. En esta guía, exploramos las diferencias clave entre Aplicaciones de Múltiples Páginas (MPA), Aplicaciones de Página Única (SPA), enfoques isomórficos influenciados por SPA y Ejecución Dividida influenciada por MPA, con visuales, ejemplos de código y diagramas.

## Aplicaciones de Múltiples Páginas (MPA)

<Note>
  Las MPA recargan la página en cada navegación.
</Note>

<img className="block h-320 dark:hidden" src="https://mintcdn.com/mapptech/gm742MzjC2-MV-lv/images/mpa_light_es.png?fit=max&auto=format&n=gm742MzjC2-MV-lv&q=85&s=06e0c9d3965da3c7df65f965395de908" alt="Diagrama MPA Light" width="1848" height="1702" data-path="images/mpa_light_es.png" />

<img className="hidden h-320 dark:block" src="https://mintcdn.com/mapptech/gm742MzjC2-MV-lv/images/mpa_dark_es.png?fit=max&auto=format&n=gm742MzjC2-MV-lv&q=85&s=82ee8c571efe305837284c33ed6fa6f6" alt="Diagrama MPA Dark" width="1848" height="1702" data-path="images/mpa_dark_es.png" />

**Pros:**

* Arquitectura simple.
* Se entrega HTML completo en la primera carga.
* Cuando las páginas son mayormente estáticas, pueden ser almacenadas en caché y entregadas eficazmente por una CDN.

**Contras:**

* La navegación es más lenta debido a recargas completas de la página.
* Las actualizaciones dinámicas requieren recargas completas de la página.

***

## Aplicaciones de Página Única (SPA)

<Note>
  Las SPA cargan contenido de forma dinámica en el cliente. Esto significa que el HTML inicial es esencialmente vacío.
</Note>

<Tabs>
  <Tab title="Diagrama">
    <img className="block h-320 dark:hidden" src="https://mintcdn.com/mapptech/gm742MzjC2-MV-lv/images/spa_light_es.png?fit=max&auto=format&n=gm742MzjC2-MV-lv&q=85&s=a65dfb969189f58327c3f76acdb95cc7" alt="Diagrama SPA Light" width="1852" height="1979" data-path="images/spa_light_es.png" />

    <img className="hidden h-320 dark:block" src="https://mintcdn.com/mapptech/gm742MzjC2-MV-lv/images/spa_dark_es.png?fit=max&auto=format&n=gm742MzjC2-MV-lv&q=85&s=99699f84def55d05bbf14ed301ab3912" alt="Diagrama SPA Dark" width="1852" height="1979" data-path="images/spa_dark_es.png" />
  </Tab>

  <Tab title="Diagrama de React">
    <img className="block h-320 dark:hidden" src="https://mintcdn.com/mapptech/gm742MzjC2-MV-lv/images/react_vanilla_light.png?fit=max&auto=format&n=gm742MzjC2-MV-lv&q=85&s=c88d13c4711889b7d6bac7240fc09e72" alt="Diagrama React Vanilla Light" width="3242" height="708" data-path="images/react_vanilla_light.png" />

    <img className="hidden h-320 dark:block" src="https://mintcdn.com/mapptech/gm742MzjC2-MV-lv/images/react_vanilla_dark.png?fit=max&auto=format&n=gm742MzjC2-MV-lv&q=85&s=f39526c13b618db5c0fc3afaa648def3" alt="Diagrama React Vanilla Dark" width="3242" height="708" data-path="images/react_vanilla_dark.png" />
  </Tab>

  <Tab title="Código">
    <CodeGroup>
      ```jsx tutorial-code.jsx theme={null}
      function App() {
        const [data, setData] = React.useState(null);
        const [query, setQuery] = React.useState("default");
        const [input, setInput] = React.useState("default");
        const [loading, setLoading] = React.useState(true);

        React.useEffect(() => {
          setLoading(true);
          fetch(
            `https://api.example.com/data?query=${encodeURIComponent(query)}`
          )
            .then((response) => response.json())
            .then((result) => {
              setData(result);
              setLoading(false);
            })
            .catch((error) => {
              console.error(error);
              setLoading(false);
            });
        }, [query]);

        const handleSubmit = (e) => {
          e.preventDefault();
          setQuery(input);
        };

        return (
          <div>
            <h1>Data Fetch Tutorial</h1>
            <form onSubmit={handleSubmit}>
              <input
                value={input}
                onChange={(e) => setInput(e.target.value)}
                placeholder="Enter query"
              />
              <button type="submit">Search</button>
            </form>
            {loading ? (
              <p>Loading data...</p>
            ) : (
              <pre>{JSON.stringify(data, null, 2)}</pre>
            )}
          </div>
        );
      }
      ```

      ```jsx actual-code.jsx theme={null}
      const DataContext = React.createContext();

      function DataProvider({ children }) {
        const [data, setData] = React.useState(null);
        const [loading, setLoading] = React.useState(false);
        const [error, setError] = React.useState(null);
        const [query, setQuery] = React.useState("default");

        React.useEffect(() => {
          const controller = new AbortController();
          const signal = controller.signal;

          async function fetchData() {
            setLoading(true);
            setError(null);
            try {
              const response = await fetch(
                `https://api.example.com/data?query=${encodeURIComponent(query)}`,
                { signal }
              );
              if (!response.ok) throw new Error("Network response was not ok");
              const result = await response.json();
              const processedData = result.map((item) => ({
                ...item,
                processed: true,
              }));
              setData(processedData);
            } catch (err) {
              if (err.name !== "AbortError") setError(err);
            } finally {
              setLoading(false);
            }
          }
          fetchData();
          return () => controller.abort();
        }, [query]);

        return (
          <DataContext.Provider value={{ data, loading, error, query, setQuery }}>
            {children}
          </DataContext.Provider>
        );
      }

      function DisplayData() {
        const { data, loading, error, query, setQuery } = React.useContext(
          DataContext
        );
        const [input, setInput] = React.useState(query);

        const handleSubmit = (e) => {
          e.preventDefault();
          setQuery(input);
        };

        return (
          <div>
            <form onSubmit={handleSubmit}>
              <input
                value={input}
                onChange={(e) => setInput(e.target.value)}
                placeholder="Enter query"
              />
              <button type="submit">Search</button>
            </form>
            {loading && <p>Loading data...</p>}
            {error && <p>Error: {error.message}</p>}
            {data && <pre>{JSON.stringify(data, null, 2)}</pre>}
          </div>
        );
      }

      function App() {
        return (
          <DataProvider>
            <h1>Advanced SPA Data Fetching Example</h1>
            <DisplayData />
          </DataProvider>
        );
      }
      ```

      ```jsx react-query.jsx theme={null}
      function DataComponent({ query }) {
        const { data, isLoading, error } = useQuery(
          ["fetchData", query],
          () =>
            fetch(
              `https://api.example.com/data?query=${encodeURIComponent(query)}`
            ).then((res) => res.json())
        );
        if (isLoading) return <p>Loading with React Query...</p>;
        if (error) return <p>Error loading data</p>;
        return <pre>{JSON.stringify(data, null, 2)}</pre>;
      }

      function App() {
        const [query, setQuery] = React.useState("default");
        const [input, setInput] = React.useState("default");

        const handleSubmit = (e) => {
          e.preventDefault();
          setQuery(input);
        };

        return (
          <div>
            <h1>SPA using React Query</h1>
            <form onSubmit={handleSubmit}>
              <input
                value={input}
                onChange={(e) => setInput(e.target.value)}
                placeholder="Enter query"
              />
              <button type="submit">Search</button>
            </form>
            <DataComponent query={query} />
          </div>
        );
      }
      ```
    </CodeGroup>
  </Tab>
</Tabs>

**Pros:**

* Navegación rápida dentro de la página.
* Interactividad dinámica.

**Contras:**

* HTML inicial mínimo.
* Fuerte dependencia del JavaScript del lado del cliente.

***

## SPA-influenciado Isomórfico

<Note>
  El HTML inicial se renderiza en el servidor para SEO y una primera carga rápida, luego se hidrata en el cliente para habilitar la interactividad.
</Note>

<Tabs>
  <Tab title="Diagrama">
    <img className="block h-320 dark:hidden" src="https://mintcdn.com/mapptech/gm742MzjC2-MV-lv/images/spa_iso_light_es.png?fit=max&auto=format&n=gm742MzjC2-MV-lv&q=85&s=2fb56e4f89b12a1c305d20d92803cc18" alt="Diagrama SPA ISO Light" width="2591" height="2046" data-path="images/spa_iso_light_es.png" />

    <img className="hidden h-320 dark:block" src="https://mintcdn.com/mapptech/gm742MzjC2-MV-lv/images/spa_iso_dark_es.png?fit=max&auto=format&n=gm742MzjC2-MV-lv&q=85&s=ea504c54f7fb60586d04d8dd9a7c8427" alt="Diagrama SPA ISO Dark" width="2591" height="2046" data-path="images/spa_iso_dark_es.png" />
  </Tab>

  <Tab title="Diagrama de React">
    <img className="block h-320 dark:hidden" src="https://mintcdn.com/mapptech/gm742MzjC2-MV-lv/images/react_skeleton_light.png?fit=max&auto=format&n=gm742MzjC2-MV-lv&q=85&s=17b5a847f4579555df97da4a047a4995" alt="Diagrama React Skeleton Light" width="3242" height="708" data-path="images/react_skeleton_light.png" />

    <img className="hidden h-320 dark:block" src="https://mintcdn.com/mapptech/gm742MzjC2-MV-lv/images/react_skeleton_dark.png?fit=max&auto=format&n=gm742MzjC2-MV-lv&q=85&s=3221aea2571144eaf84c318810d75997" alt="Diagrama React Skeleton Dark" width="3242" height="708" data-path="images/react_skeleton_dark.png" />
  </Tab>

  <Tab title="Código">
    <CodeGroup>
      ```jsx page.jsx theme={null}
      export const metadata = {
        title: "SPA-influenciado Isomorphic Rendering Example",
        description: "An isomorphic rendering example with SEO metadata.",
        keywords: "rendering, isomorphic, react, SEO, server components",
      };

      // Assume MutationManager is dynamically imported for client-side behavior.
      export default async function Page({ searchParams }) {
        const query = searchParams.query || "default";
        const res = await fetch(
          `https://api.example.com/data?query=${encodeURIComponent(query)}`,
          { cache: "force-cache" }
        );
        const data = await res.json();
        return (
          <div>
            <h1>Isomorphic Page - Results for: {query}</h1>
            <pre>{JSON.stringify(data, null, 2)}</pre>
            <MutationManager />
          </div>
        );
      }
      ```

      ```jsx loading.jsx theme={null}
      export default function Loading() {
        return <p>Loading...</p>;
      }
      ```

      ```jsx error.jsx theme={null}
      "use client";
      export default function Error({ error, reset }) {
        return (
          <div>
            <h1>Error Loading Page</h1>
            <p>{error.message}</p>
            <button onClick={reset}>Try again</button>
          </div>
        );
      }
      ```

      ```jsx MutationManager.jsx theme={null}
      "use client";
      import { useState } from "react";
      import { useMutation, useQueryClient } from "react-query";

      async function updateData(newValue) {
        const res = await fetch("/api/update", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ value: newValue }),
        });
        if (!res.ok) throw new Error("Update failed");
        return res.json();
      }

      export default function MutationManager() {
        const [input, setInput] = useState("");
        const queryClient = useQueryClient();
        const mutation = useMutation(updateData, {
          onSuccess: () => queryClient.invalidateQueries("fetchData"),
        });

        const handleSubmit = (e) => {
          e.preventDefault();
          mutation.mutate(input);
        };

        if (mutation.error) {
          return (
            <div>
              <p>Error updating data.</p>
            </div>
          );
        }

        if (mutation.isLoading) {
          return (
            <div>
              <p>Updating...</p>
            </div>
          );
        }

        return (
          <div>
            <form onSubmit={handleSubmit}>
              <input
                value={input}
                onChange={(e) => setInput(e.target.value)}
                placeholder="Enter new value"
              />
              <button type="submit">Submit</button>
            </form>
          </div>
        );
      }
      ```
    </CodeGroup>
  </Tab>
</Tabs>

**Pros:**

* Se entrega HTML completo y amigable con SEO inicialmente.

**Contras:**

* Requiere coordinación entre el cliente y el servidor.
* Configuración más compleja.

***

## MPA-influenciado por Ejecución Dividida

<Note>
  HTML en su mayoría/completo renderizado en el servidor utilizando acciones del servidor.
</Note>

<Tabs>
  <Tab title="Diagrama">
    <img className="block h-320 dark:hidden" src="https://mintcdn.com/mapptech/gm742MzjC2-MV-lv/images/mpa_split_light_es.png?fit=max&auto=format&n=gm742MzjC2-MV-lv&q=85&s=9c7de1faebdc91294e90b4a94f00658e" alt="Diagrama MPA Split Light" width="2406" height="1714" data-path="images/mpa_split_light_es.png" />

    <img className="hidden h-320 dark:block" src="https://mintcdn.com/mapptech/gm742MzjC2-MV-lv/images/mpa_split_dark_es.png?fit=max&auto=format&n=gm742MzjC2-MV-lv&q=85&s=e3731ec4b3359c2d96e83723f9890255" alt="Diagrama MPA Split Dark" width="2406" height="1714" data-path="images/mpa_split_dark_es.png" />
  </Tab>

  <Tab title="Diagrama de React">
    <AccordionGroup>
      <Accordion title="Diagrama React Server" icon="server">
        <img className="block h-320 dark:hidden" src="https://mintcdn.com/mapptech/gm742MzjC2-MV-lv/images/react_server_light.png?fit=max&auto=format&n=gm742MzjC2-MV-lv&q=85&s=4bd3c4b778bd2fc8c021ecb870ee2cc7" alt="Diagrama React Server Light" width="3242" height="708" data-path="images/react_server_light.png" />

        <img className="hidden h-320 dark:block" src="https://mintcdn.com/mapptech/gm742MzjC2-MV-lv/images/react_server_dark.png?fit=max&auto=format&n=gm742MzjC2-MV-lv&q=85&s=9f1c3998ddaf4bf4c2b1b44c00d7a353" alt="Diagrama React Server Dark" width="3242" height="708" data-path="images/react_server_dark.png" />
      </Accordion>

      <Accordion title="Diagrama React Lean" icon="code">
        <img className="block h-320 dark:hidden" src="https://mintcdn.com/mapptech/gm742MzjC2-MV-lv/images/react_lean_light.png?fit=max&auto=format&n=gm742MzjC2-MV-lv&q=85&s=0aa02e392a4a13f9e2898ee8836f8ce0" alt="Diagrama React Lean Light" width="3242" height="708" data-path="images/react_lean_light.png" />

        <img className="hidden h-320 dark:block" src="https://mintcdn.com/mapptech/gm742MzjC2-MV-lv/images/react_lean_dark.png?fit=max&auto=format&n=gm742MzjC2-MV-lv&q=85&s=a6d1addcfef5f7ff3793d7095240bf23" alt="Diagrama React Lean Dark" width="3242" height="708" data-path="images/react_lean_dark.png" />
      </Accordion>
    </AccordionGroup>
  </Tab>

  <Tab title="Código">
    <CodeGroup>
      ```jsx page.jsx theme={null}
      export const metadata = {
        title: "Server-Rendered Page with Actions",
        description:
          "A server-rendered page that handles mutations using server actions and revalidates paths.",
        keywords: "rendering, server actions, Next.js, MPA",
      };

      export default async function Page({ searchParams }) {
        const query = searchParams.query || "default";
        const res = await fetch(
          `https://api.example.com/data?query=${encodeURIComponent(query)}`,
          { cache: "force-cache" }
        );
        const data = await res.json();

        async function handleMutation(formData) {
          "use server";
          const res = await fetch("https://api.example.com/update", {
            method: "POST",
            body: formData,
          });
          revalidatePath("/");
        }

        return (
          <div>
            <h1>Server-Rendered Page with Actions</h1>
            <pre>{JSON.stringify(data, null, 2)}</pre>
            <ErrorBoundary fallback={<p>Error updating data.</p>}>
              <Suspense fallback={<p>Updating...</p>}>
                <form action={handleMutation}>
                  <input name="value" type="text" />
                  <button type="submit">Submit</button>
                </form>
              </Suspense>
            </ErrorBoundary>
          </div>
        );
      }
      ```

      ```jsx loading.jsx theme={null}
      export default function Loading() {
        return <p>Loading...</p>;
      }
      ```

      ```jsx error.jsx theme={null}
      "use client";
      export default function Error({ error, reset }) {
        return (
          <div>
            <h1>Error Loading Page</h1>
            <p>{error.message}</p>
            <button onClick={reset}>Try again</button>
          </div>
        );
      }
      ```
    </CodeGroup>
  </Tab>
</Tabs>

**Pros:**

* Entrega de HTML amigable con CDN.
* Código mínimo del lado del cliente.
* Excelente SEO.

**Contras:**

* La interactividad del lado del cliente puede requerir manejo adicional.
* Aumento del consumo de CPU del servidor.

***

## 📊 Comparación de Estrategias de Renderizado

| Propiedad                     | MPA | SPA | SPA Isomórfico | MPA Ejecución Dividida |
| ----------------------------- | :-: | :-: | :------------: | :--------------------: |
| **SEO Amigable**              |  ✅  |  ❌  |        ✅       |            ✅           |
| **Navegación Rápida**         |  ❌  |  ✅  |        ✅       |            ✅           |
| **Experiencia Interactiva**   |  ❌  |  ✅  |        ✅       |            ✅           |
| **Amigable con CDN**          |  ✅  |  ❌  |        ✅       |            ✅           |
| **Arquitectura Simple**       |  ✅  |  ❌  |        ❌       |            ✅           |
| **JS Cliente Ligero**         |  ✅  |  ❌  |        ✅       |            ✅           |
| **Bajo Uso de CPU**           |  ❌  |  ✅  |        ✅       |            ❌           |
| **Bajos Costos del Servidor** |  ❌  |  ✅  |        ❌       |            ✅           |

***

## 🚀 Ejemplo en Vivo: NextFaster

Para ver estas estrategias de renderizado en acción, consulta **[NextFaster](https://next-faster.vercel.app)** — un proyecto del mundo real que combina componentes del servidor, islas de React y ejecución dividida para un rendimiento y SEO óptimos.

<div style={{ position: "relative", marginTop: "1.5rem", borderRadius: "12px", overflow: "hidden", border: "1px solid #ccc" }}>
  <div
    style={{
position: "absolute",
top: "0.5rem",
right: "0.5rem",
display: "flex",
gap: "0.25rem",
zIndex: 10
}}
  >
    <button
      onClick={() => window.open("https://next-faster.vercel.app", "_blank")}
      title="Abrir en una nueva pestaña"
      style={{
    background: "white",
    border: "1px solid #eee",
    borderRadius: "6px",
    padding: "0.3rem",
    cursor: "pointer",
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    boxShadow: "0 1px 3px rgba(0,0,0,0.05)"
  }}
    >
      <Icon icon="arrow-up-right-from-square" iconType="solid" color="#0070f3" />
    </button>

    <button
      onClick={() => window.open("https://github.com/ethanniser/NextFaster", "_blank")}
      title="Ver repositorio en GitHub"
      style={{
    background: "white",
    border: "1px solid #eee",
    borderRadius: "6px",
    padding: "0.3rem",
    cursor: "pointer",
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    boxShadow: "0 1px 3px rgba(0,0,0,0.05)"
  }}
    >
      <Icon icon="github" iconType="brands" color="#000000" />
    </button>
  </div>

  <iframe src="https://next-faster.vercel.app" width="100%" height="600px" style={{ border: "none", display: "block" }} />
</div>

**¿Qué hace a NextFaster interesante?**

* ⚙️ **Componentes del Servidor de React** con JavaScript del lado del cliente mínimo
* 🔄 **Acciones del Servidor** y **Revalidación en el Borde**
* 💸 **Costos de alojamiento excepcionalmente bajos** junto con alto rendimiento
* 🔍 Excelente equilibrio entre **SEO y rendimiento**

> Ideal para estudiar técnicas modernas de renderizado en Next.js 14+ con App Router.

## 📺 Videos Recomendados

<CardGroup cols={2}>
  <Card title="Estrategias de Consultar Datos" icon="server" href="https://youtu.be/51pf_nCJpwg">
    <a href="https://youtu.be/51pf_nCJpwg" target="_blank" rel="noopener noreferrer" className="flex flex-col h-full">
      <img src="https://img.youtube.com/vi/51pf_nCJpwg/hqdefault.jpg" alt="Por qué no uso React-Query y tRPC en Next.js" className="rounded-xl" noZoom />

      <div className="mt-auto">
        <Icon icon="youtube" iconType="brands" /> Ver en YouTube
      </div>
    </a>
  </Card>

  <Card title="Carga de Datos en Next.js 15" icon="download" href="https://youtu.be/nfTv6arvLZc">
    <a href="https://youtu.be/nfTv6arvLZc" target="_blank" rel="noopener noreferrer" className="flex flex-col h-full">
      <img src="https://img.youtube.com/vi/nfTv6arvLZc/hqdefault.jpg" alt="Carga de Datos en Next.js 15" className="rounded-xl" noZoom />

      <div className="mt-auto">
        <Icon icon="youtube" iconType="solid" /> Ver en YouTube
      </div>
    </a>
  </Card>

  <Card title="Componentes de Servidor" icon="compass" href="https://youtu.be/rGPpQdbDbwo">
    <a href="https://youtu.be/rGPpQdbDbwo" target="_blank" rel="noopener noreferrer" className="flex flex-col h-full">
      <img src="https://img.youtube.com/vi/rGPpQdbDbwo/hqdefault.jpg" alt="Los Componentes del Servidor de React lo Cambian Todo" className="rounded-xl" noZoom />

      <div className="mt-auto">
        <Icon icon="youtube" iconType="brands" /> Ver en YouTube
      </div>
    </a>
  </Card>

  <Card title="SSR vs CSR: Trampas de SEO" icon="magnifying-glass" href="https://youtu.be/YfkJyRFvP1s">
    <a href="https://youtu.be/YfkJyRFvP1s" target="_blank" rel="noopener noreferrer" className="flex flex-col h-full">
      <img src="https://img.youtube.com/vi/YfkJyRFvP1s/hqdefault.jpg" alt="Renderizado del Lado del Servidor vs. Renderizado del Lado del Cliente. ¡PROBLEMAS de SEO!" className="rounded-xl" noZoom />

      <div className="mt-auto">
        <Icon icon="youtube" iconType="brands" /> Ver en YouTube
      </div>
    </a>
  </Card>
</CardGroup>

## 📚 Lecturas Adicionales

* [React Server Components](https://react.dev/reference/rsc/server-components)
* [React Query](https://react-query.tanstack.com/)
* [Documentación de Next.js](https://nextjs.org/docs)
* [Acciones del Servidor en Next.js](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions)
* [React Query vs React Server Components](https://www.reddit.com/r/nextjs/comments/19d0sar/nextjs_14_server_actions_vs_react_query/)

## 🧠 Reflexiones Finales

Cada estrategia de renderizado tiene sus fortalezas. Las MPA son sencillas y, cuando son mayormente estáticas, pueden ser almacenadas en caché de manera eficaz mediante una CDN; las SPA ofrecen interactividad dinámica enteramente en el cliente (resultando en un HTML inicial mínimo); los enfoques isomórficos influenciados por SPA combinan SEO y actualizaciones dinámicas al renderizar el HTML inicial en el servidor y posteriormente hidratarlo en el cliente; y la Ejecución Dividida influenciada por MPA ofrece rendimiento con mutaciones manejadas en el servidor utilizando acciones del servidor.
