Voltar para o Blog

Server-First Development: SvelteKit, Astro e Remix Dominam 2025

Olá HaWkers, a era do client-side everything acabou. Em 2025, frameworks server-first como SvelteKit, Astro e Remix estão dominando desenvolvimento web moderno — e por boas razões.

70% menos JavaScript no cliente, Core Web Vitals perfeitos, SEO natural e DX (developer experience) superior. Vamos destrinchar esses frameworks e entender quando usar cada um.

O Que É Server-First Development?

Client-First vs Server-First

// Entendendo o paradigm shift

const paradigmComparison = {
  clientFirst: {
    // React SPA tradicional, Vue SPA, Angular
    architecture: "Browser faz tudo (rendering, routing, data fetching)",

    flow: [
      "1. Browser recebe HTML vazio + bundle JS gigante",
      "2. Download e parse do JS (3-7s)",
      "3. React hydrates e renderiza",
      "4. Fetch data (APIs)",
      "5. Re-render com dados",
    ],

    example: `
      // Cliente recebe:
      <div id="root"></div>
      <script src="bundle.js"></script> <!-- 500kb+ -->

      // Usuário vê tela branca até JS carregar e executar
    `,

    pros: [
      "✅ Interatividade rica após carregar",
      "✅ Transições suaves (SPA)",
      "✅ Menos carga no servidor",
    ],

    cons: [
      "❌ TTI (Time to Interactive) lento (3-7s)",
      "❌ SEO ruim (requer workarounds)",
      "❌ Bundle size grande",
      "❌ Blank screen inicial",
      "❌ Performance ruim em mobile",
    ],

    coreWebVitals: {
      lcp: "3.5-7s (poor)", // Largest Contentful Paint
      fid: "100-300ms (needs improvement)", // First Input Delay
      cls: "0.1-0.25 (needs improvement)", // Cumulative Layout Shift
    },
  },

  serverFirst: {
    // SvelteKit, Astro, Remix, Next.js App Router
    architecture: "Server renderiza HTML completo, JavaScript mínimo no cliente",

    flow: [
      "1. Browser requisita página",
      "2. Server renderiza HTML completo (com dados)",
      "3. Browser exibe conteúdo IMEDIATAMENTE",
      "4. JS mínimo hidrata interatividade (se necessário)",
    ],

    example: `
      // Cliente recebe:
      <html>
        <body>
          <h1>Products</h1>
          <div>
            <!-- Conteúdo completo já renderizado -->
            <article>Product 1 - $29.99</article>
            <article>Product 2 - $39.99</article>
          </div>
        </body>
        <script src="islands.js"></script> <!-- 20kb, apenas interatividade -->
      </html>

      // Usuário vê conteúdo em <1s
    `,

    pros: [
      "✅ TTI ultra-rápido (0.5-2s)",
      "✅ SEO perfeito (HTML completo no first paint)",
      "✅ Bundle size mínimo (70-90% menor)",
      "✅ Performance mobile excelente",
      "✅ Funciona com JS desabilitado (progressive enhancement)",
    ],

    cons: [
      "❌ Carga maior no servidor (mas mitigável com cache)",
      "❌ Navegação não é instantânea como SPA (mas prefetch resolve)",
    ],

    coreWebVitals: {
      lcp: "0.8-2s (good)",
      fid: "<100ms (good)",
      cls: "<0.1 (good)",
    },
  },
};

SvelteKit: O Elegante e Rápido

Por Que SvelteKit Está Explodindo em 2025

const svelteKitOverview = {
  tagline: "Web development, streamlined",

  philosophy: [
    "Compiler-based (sem virtual DOM = menos overhead)",
    "Write less code (menos boilerplate que React)",
    "Server-first com client hydration seletiva",
    "File-based routing (como Next.js)",
  ],

  performance: {
    bundleSize: "70% menor que React equivalente",
    runtime: "Sem framework runtime (compila para vanilla JS)",
    hydration: "Só hidrata o que precisa de interatividade",
  },

  adoption2025: {
    satisfaction: "95% (maior de todos os frameworks)",
    growth: "+156% em usage year-over-year",
    companies: ["New York Times", "Spotify", "1Password", "Chess.com"],
  },
};

SvelteKit: Código Real

<!-- +page.server.ts: Server-side data loading -->
<script lang="ts">
  import type { PageServerLoad } from './$types';

  export const load: PageServerLoad = async ({ fetch, params }) => {
    // Roda NO SERVER (antes de enviar HTML)
    const response = await fetch('/api/products');
    const products = await response.json();

    return {
      products // Dados disponíveis imediatamente no componente
    };
  };
</script>

<!-- +page.svelte: Componente -->
<script lang="ts">
  import { enhance } from '$app/forms';
  import type { PageData } from './$types';

  export let data: PageData;

  // Client-side reactive state (opcional)
  let searchQuery = '';

  $: filteredProducts = data.products.filter(p =>
    p.name.toLowerCase().includes(searchQuery.toLowerCase())
  );
</script>

<div class="products-page">
  <h1>Products ({filteredProducts.length})</h1>

  <!-- Reactive search (client-side) -->
  <input
    type="search"
    bind:value={searchQuery}
    placeholder="Search products..."
  />

  <div class="product-grid">
    {#each filteredProducts as product (product.id)}
      <article class="product-card">
        <img src={product.image} alt={product.name} />
        <h2>{product.name}</h2>
        <p>{product.description}</p>
        <span class="price">${product.price}</span>

        <!-- Form com progressive enhancement -->
        <form method="POST" action="?/addToCart" use:enhance>
          <input type="hidden" name="productId" value={product.id} />
          <button type="submit">Add to Cart</button>
        </form>
      </article>
    {/each}
  </div>
</div>

<style>
  /* Scoped CSS (não vaza para outros componentes) */
  .product-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 2rem;
  }

  .product-card {
    border: 1px solid #eee;
    padding: 1rem;
    border-radius: 8px;
  }

  .price {
    font-size: 1.5rem;
    font-weight: bold;
    color: #007bff;
  }
</style>
// +page.server.ts: Server actions (form handling)

import type { Actions } from "./$types";
import { fail } from "@sveltejs/kit";

export const actions: Actions = {
  // POST form action (funciona sem JS!)
  addToCart: async ({ request, cookies }) => {
    const formData = await request.formData();
    const productId = formData.get("productId");

    if (!productId) {
      return fail(400, { message: "Product ID required" });
    }

    // Adicionar ao carrinho (server-side)
    const cart = JSON.parse(cookies.get("cart") || "[]");
    cart.push(productId);
    cookies.set("cart", JSON.stringify(cart), { path: "/" });

    return { success: true };
  },
};

SvelteKit: Vantagens Únicas

const svelteKitStrengths = {
  devExperience: {
    lessCode: {
      react: `
        const [count, setCount] = useState(0);

        <button onClick={() => setCount(count + 1)}>
          {count}
        </button>
      `,
      svelte: `
        let count = 0;

        <button on:click={() => count++}>
          {count}
        </button>
      `,
      savings: "-40% menos código para mesma funcionalidade",
    },

    reactivity: "Built-in (não precisa de useState, useEffect, etc)",
    css: "Scoped por padrão (sem CSS-in-JS overhead)",
    animations: "Built-in (transition, animate directives)",
  },

  performance: {
    noVirtualDOM: "Compila para operações imperativas (mais rápido)",
    bundleSize: "TodoMVC: Svelte 3.6kb | React 40kb",
    hydration: "Partial hydration automático",
  },

  fullStack: {
    adapters: [
      "Vercel",
      "Netlify",
      "Cloudflare Workers",
      "Node.js",
      "Static (SSG)",
    ],
    apiRoutes: "Built-in (como Next.js)",
    serverActions: "Progressive enhancement (funciona sem JS)",
  },
};

Astro: Content-First Champion

Quando Astro É a Escolha Perfeita

const astroOverview = {
  tagline: "The web framework for content-driven websites",

  philosophy: [
    "Ship ZERO JavaScript por padrão",
    "Islands Architecture (hidrata apenas componentes interativos)",
    "Framework-agnostic (use React, Vue, Svelte juntos!)",
    "Content collections (Markdown, MDX, CMS)",
  ],

  idealFor: [
    "Blogs, documentação, marketing sites",
    "E-commerce (Shopify, Stripe)",
    "Dashboards (com islands interativos)",
    "Qualquer site content-heavy",
  ],

  performance: {
    jsShipped: "0kb por padrão (adiciona apenas onde necessário)",
    lcp: "0.5-1.5s (fastest of all frameworks)",
    lighthouseScore: "98-100 (consistente)",
  },

  adoption2025: {
    satisfaction: "92%",
    growth: "+210% year-over-year (fastest growing)",
    companies: ["Google Firebase Docs", "The Guardian", "Trivago"],
  },
};

Astro: Código Real

---
// src/pages/blog/[slug].astro
// Código no topo roda NO BUILD TIME (SSG) ou SERVER (SSR)

import Layout from '../../layouts/Layout.astro';
import { getCollection } from 'astro:content';

// Static paths (SSG)
export async function getStaticPaths() {
  const blogPosts = await getCollection('blog');

  return blogPosts.map(post => ({
    params: { slug: post.slug },
    props: { post }
  }));
}

const { post } = Astro.props;
const { Content } = await post.render();
---

<!-- HTML enviado ao cliente (ZERO JavaScript!) -->
<Layout title={post.data.title}>
  <article class="blog-post">
    <header>
      <h1>{post.data.title}</h1>
      <time datetime={post.data.publishDate.toISOString()}>
        {post.data.publishDate.toLocaleDateString()}
      </time>
    </header>

    <!-- Markdown renderizado como HTML -->
    <Content />

    <!-- Island: só este componente tem JS -->
    <LikeButton client:visible postId={post.slug} />

    <!-- Comments só carrega quando visível -->
    <Comments client:idle postId={post.slug} />
  </article>
</Layout>

<style>
  .blog-post {
    max-width: 65ch;
    margin: 0 auto;
    padding: 2rem;
  }

  h1 {
    font-size: 2.5rem;
    margin-bottom: 1rem;
  }
</style>
// components/LikeButton.tsx (React island)
// Este componente TEM JavaScript (hidratado no cliente)

import { useState, useEffect } from "react";

interface Props {
  postId: string;
}

export default function LikeButton({ postId }: Props) {
  const [likes, setLikes] = useState(0);
  const [hasLiked, setHasLiked] = useState(false);

  useEffect(() => {
    // Fetch likes count
    fetch(`/api/likes/${postId}`)
      .then((r) => r.json())
      .then((data) => setLikes(data.count));
  }, [postId]);

  const handleLike = async () => {
    if (hasLiked) return;

    await fetch(`/api/likes/${postId}`, { method: "POST" });
    setLikes((l) => l + 1);
    setHasLiked(true);
  };

  return (
    <button
      onClick={handleLike}
      disabled={hasLiked}
      className={hasLiked ? "liked" : ""}
    >
      ❤️ {likes} {likes === 1 ? "Like" : "Likes"}
    </button>
  );
}
// src/content/config.ts: Content collections (type-safe)

import { defineCollection, z } from "astro:content";

const blogCollection = defineCollection({
  type: "content", // Markdown/MDX
  schema: z.object({
    title: z.string(),
    description: z.string(),
    publishDate: z.date(),
    author: z.string(),
    tags: z.array(z.string()),
    image: z.string().optional(),
  }),
});

export const collections = {
  blog: blogCollection,
};

// Uso: type-safe em toda a codebase!
// const post = await getEntry('blog', 'my-post');
// post.data.title // TypeScript sabe que é string

Astro: Client Directives (Power Feature)

---
// Controle PRECISO de quando hidratar componentes
import HeavyComponent from './HeavyComponent';
import Sidebar from './Sidebar';
import Analytics from './Analytics';
---

<!-- ZERO JS: apenas HTML estático -->
<Header />

<!-- Hidrata imediatamente (critical UI) -->
<SearchBar client:load />

<!-- Hidrata quando visível no viewport -->
<HeavyComponent client:visible />

<!-- Hidrata quando browser estiver idle -->
<Sidebar client:idle />

<!-- Hidrata apenas quando media query match -->
<MobileMenu client:media="(max-width: 768px)" />

<!-- NUNCA hidrata (apenas HTML) -->
<Footer />

<!-- JS roda apenas no servidor (0kb no cliente) -->
<Analytics />

<!--
  Resultado:
  - Sem directives: 200kb JS
  - Com directives: 20kb JS (-90%)
-->

Remix: Full-Stack React Reinventado

O Que Torna Remix Único

const remixOverview = {
  tagline: "Build Better Websites",

  philosophy: [
    "Embrace the web platform (forms, URLs, HTTP)",
    "Progressive enhancement first",
    "Nested routing (colocation de UI e data)",
    "Optimistic UI fácil",
  ],

  createdBy: "Michael Jackson e Ryan Florence (React Router creators)",

  acquiredBy: "Shopify (2022) - agora open-source e gratuito",

  idealFor: [
    "Full-stack apps (não apenas sites estáticos)",
    "E-commerce (integração Shopify)",
    "Dashboards complexos",
    "Apps com muita interação",
  ],

  adoption2025: {
    satisfaction: "89%",
    growth: "+85% (pós-aquisição Shopify)",
    companies: ["Shopify", "NASA", "Peloton", "GitHub Mobile"],
  },
};

Remix: Código Real

// app/routes/products.$productId.tsx
// Loader: roda no servidor

import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData, useFetcher } from "@remix-run/react";

export async function loader({ params }: LoaderFunctionArgs) {
  const product = await db.product.findUnique({
    where: { id: params.productId },
    include: { reviews: true },
  });

  if (!product) {
    throw new Response("Not Found", { status: 404 });
  }

  return json({ product });
}

// Action: form submissions (POST, PUT, DELETE)
export async function action({ request, params }: ActionFunctionArgs) {
  const formData = await request.formData();
  const intent = formData.get("intent");

  if (intent === "addToCart") {
    const quantity = formData.get("quantity");

    await addToCart({
      productId: params.productId,
      quantity: Number(quantity),
    });

    return json({ success: true });
  }

  if (intent === "submitReview") {
    const rating = formData.get("rating");
    const comment = formData.get("comment");

    await db.review.create({
      data: {
        productId: params.productId,
        rating: Number(rating),
        comment: String(comment),
      },
    });

    // Revalida loader automaticamente (UI atualiza)
    return json({ success: true });
  }

  throw new Error("Invalid intent");
}

// Component
export default function ProductPage() {
  const { product } = useLoaderData<typeof loader>();
  const fetcher = useFetcher();

  return (
    <div className="product-page">
      <img src={product.image} alt={product.name} />
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <span className="price">${product.price}</span>

      {/* Form com progressive enhancement */}
      <fetcher.Form method="post">
        <input type="hidden" name="intent" value="addToCart" />
        <input
          type="number"
          name="quantity"
          min="1"
          defaultValue="1"
          required
        />
        <button type="submit" disabled={fetcher.state !== "idle"}>
          {fetcher.state === "submitting" ? "Adding..." : "Add to Cart"}
        </button>
      </fetcher.Form>

      {/* Reviews */}
      <section className="reviews">
        <h2>Reviews ({product.reviews.length})</h2>

        {product.reviews.map((review) => (
          <article key={review.id} className="review">
            <div className="rating">{"".repeat(review.rating)}</div>
            <p>{review.comment}</p>
          </article>
        ))}

        {/* Submit review form */}
        <fetcher.Form method="post" className="review-form">
          <input type="hidden" name="intent" value="submitReview" />

          <label>
            Rating:
            <select name="rating" required>
              <option value="5">5 stars</option>
              <option value="4">4 stars</option>
              <option value="3">3 stars</option>
              <option value="2">2 stars</option>
              <option value="1">1 star</option>
            </select>
          </label>

          <label>
            Comment:
            <textarea name="comment" required />
          </label>

          <button type="submit">Submit Review</button>
        </fetcher.Form>
      </section>
    </div>
  );
}

Remix: Nested Routing (Killer Feature)

// app/routes/_layout.tsx (parent layout)
import { Outlet } from "@remix-run/react";

export default function Layout() {
  return (
    <div>
      <header>
        <nav>{/* Navigation */}</nav>
      </header>

      <main>
        <Outlet /> {/* Child routes render aqui */}
      </main>

      <footer>{/* Footer */}</footer>
    </div>
  );
}

// app/routes/_layout.products.tsx (nested layout)
export async function loader() {
  // Carrega categories (shared entre todas as páginas de produtos)
  const categories = await db.category.findMany();
  return json({ categories });
}

export default function ProductsLayout() {
  const { categories } = useLoaderData<typeof loader>();

  return (
    <div className="products-layout">
      <aside>
        <h3>Categories</h3>
        <ul>
          {categories.map((cat) => (
            <li key={cat.id}>
              <Link to={`/products/${cat.slug}`}>{cat.name}</Link>
            </li>
          ))}
        </ul>
      </aside>

      <div className="products-content">
        <Outlet /> {/* Páginas específicas de produtos */}
      </div>
    </div>
  );
}

// app/routes/_layout.products.$category.tsx (child route)
export async function loader({ params }: LoaderFunctionArgs) {
  const products = await db.product.findMany({
    where: { categorySlug: params.category },
  });
  return json({ products });
}

export default function CategoryPage() {
  const { products } = useLoaderData<typeof loader>();

  return (
    <div className="category-products">
      {products.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

// Resultado: 3 loaders rodando em paralelo (parent + nested + child)
// UI aninhada com data loading otimizado

Comparação: SvelteKit vs Astro vs Remix

Decision Matrix

const frameworkComparison = {
  svelteKit: {
    bestFor: ["Full-stack apps", "Interactive SPAs", "Real-time apps"],
    strengths: [
      "DX incrível (menos boilerplate)",
      "Performance excelente",
      "Bundle size mínimo",
      "Reatividade built-in",
    ],
    weaknesses: [
      "Ecossistema menor que React",
      "Menos vagas de emprego (ainda)",
      "Menor comunidade",
    ],
    learningCurve: "Média (precisa aprender Svelte)",
    bundleSize: "20-50kb (típico)",
    seo: "Excelente (SSR built-in)",
  },

  astro: {
    bestFor: ["Blogs", "Marketing sites", "Docs", "Content-first"],
    strengths: [
      "ZERO JS por padrão (fastest TTI)",
      "Framework-agnostic (use qualquer UI lib)",
      "Content collections (MDX, CMS)",
      "Islands architecture",
    ],
    weaknesses: [
      "Menos ideal para apps altamente interativos",
      "Não é full-stack (precisa de API separada)",
    ],
    learningCurve: "Baixa (HTML, CSS, JS básico)",
    bundleSize: "0-20kb (típico)",
    seo: "Perfeito (HTML estático)",
  },

  remix: {
    bestFor: ["Full-stack React apps", "E-commerce", "Dashboards"],
    strengths: [
      "React ecosystem completo",
      "Progressive enhancement forte",
      "Nested routing",
      "Optimistic UI fácil",
    ],
    weaknesses: [
      "Ainda é React (bundle maior que Svelte)",
      "Menos foco em SSG (mais SSR)",
    ],
    learningCurve: "Baixa (se já sabe React)",
    bundleSize: "50-100kb (típico)",
    seo: "Excelente (SSR)",
  },

  nextJs: {
    // Para comparação
    bestFor: ["Full-stack React (industry standard)"],
    strengths: [
      "Ecossistema gigante",
      "Vercel integration",
      "App Router (server components)",
    ],
    weaknesses: ["Complexo (muitas features)", "Bundle size grande"],
    learningCurve: "Alta (App Router é confuso)",
    bundleSize: "100-200kb (típico)",
    seo: "Excelente",
  },
};

// Quando usar cada um?
const decisionTree = {
  contentSite: "Astro (blog, docs, marketing)",
  fullStackApp: "SvelteKit (new project) | Remix (React team)",
  ecommerce: "Remix (Shopify) | SvelteKit",
  dashboard: "SvelteKit | Remix",
  spa: "SvelteKit",
  multiFramework: "Astro (React + Vue + Svelte together)",
  existingReact: "Remix (migração mais fácil)",
  greenfield: "SvelteKit (melhor DX e performance)",
};

Performance Benchmark (Real World)

// Benchmark: E-commerce product page (10 products)

const performanceBenchmark = {
  traditional_spa: {
    // Create React App
    ttfb: "250ms", // Time to First Byte
    fcp: "1800ms", // First Contentful Paint
    lcp: "4500ms", // Largest Contentful Paint (FAILED)
    tti: "5200ms", // Time to Interactive (FAILED)
    bundleSize: "420kb gzipped",
    lighthouse: "52/100",
    seoScore: "Poor (client-side rendering)",
  },

  nextJs_appRouter: {
    // Next.js 15 App Router (RSC)
    ttfb: "180ms",
    fcp: "600ms",
    lcp: "1400ms", // GOOD
    tti: "2100ms", // OK
    bundleSize: "180kb gzipped",
    lighthouse: "82/100",
    seoScore: "Excellent",
  },

  svelteKit: {
    ttfb: "160ms",
    fcp: "450ms",
    lcp: "900ms", // EXCELLENT
    tti: "1100ms", // EXCELLENT
    bundleSize: "45kb gzipped",
    lighthouse: "96/100",
    seoScore: "Excellent",
  },

  astro: {
    ttfb: "140ms",
    fcp: "380ms",
    lcp: "750ms", // BEST
    tti: "800ms", // BEST
    bundleSize: "8kb gzipped", // BEST (apenas interatividade)
    lighthouse: "99/100",
    seoScore: "Perfect",
  },

  remix: {
    ttfb: "170ms",
    fcp: "520ms",
    lcp: "1100ms", // EXCELLENT
    tti: "1600ms", // GOOD
    bundleSize: "95kb gzipped",
    lighthouse: "91/100",
    seoScore: "Excellent",
  },
};

Conclusão: Server-First Venceu

O futuro do web development é server-first.

Realidade em 2025:

const serverFirstAdoption = {
  industry: {
    newProjects: "73% escolhem server-first frameworks",
    migrations: "+45% de SPAs migrando para SSR",
    reason: "Core Web Vitals = SEO = dinheiro",
  },

  yourChoice: {
    blog: "Astro (melhor performance)",
    fullStack: "SvelteKit (melhor DX) | Remix (React team)",
    ecommerce: "Remix (Shopify integration)",
    hybrid: "Next.js (ainda king, mas complexo)",
  },

  actionPlan: {
    immediate: "Experimente Astro em blog pessoal (1 fim de semana)",
    shortTerm: "Aprenda SvelteKit OU Remix (2-4 semanas)",
    longTerm: "Migre projetos críticos (ROI comprovado)",
  },

  bottomLine: "Client-first SPAs estão morrendo. Server-first é o padrão.",
};

Menos JavaScript, mais performance, melhor SEO.

Se você quer entender mais sobre performance moderna, recomendo: WebAssembly + JavaScript Performance.

Bora pra cima! 🦅

📚 Quer Dominar JavaScript Moderno?

Esses frameworks são incríveis, mas JavaScript sólido continua sendo a base. Desenvolvedores que dominam os fundamentos aproveitam melhor qualquer framework.

Material de Estudo Completo

Preparei um guia completo de JavaScript do básico ao avançado:

Opções de investimento:

  • R$9,90 (pagamento único)

👉 Conhecer o Guia JavaScript

💡 Base sólida para dominar qualquer framework moderno

Comentários (0)

Esse artigo ainda não possui comentários 😢. Seja o primeiro! 🚀🦅

Adicionar comentário