The Web Rendering Space Race: Which Meta-Framework Will Take You to the Stars?

Héla Ben Khalfallah
ekino-france
Published in
34 min readAug 9, 2024

Navigating Next.js, Astro, and Remix to Find The Perfect Fit

Astronaut working with laptop (Image licensed to the author)

🚨 Houston, We Have a Rendering Dilemma!

Choosing the right rendering strategy for our web project isn’t rocket science, but it can feel like a mission-critical decision. Server-side rendering (SSR), static site generation (SSG), and incremental static regeneration (ISR) each offer unique advantages and challenges. And with the popularity of frameworks like Next.js, Astro, and Remix, the options can seem overwhelming.

Apollo 13 calls mission control, “Houston we’ve had a problem”

At ekino-France, we’re on a mission to solve this rendering puzzle. We’ll put these frameworks to the test, examining their SSR, SSG, and ISR capabilities in detail. We’ll delve into their performance, flexibility, and developer experience, comparing them side-by-side to reveal their hidden strengths and weaknesses.

Think of this guide as our mission control manual. By the end, we’ll have all the information we need to choose the right rendering strategy and framework for our specific needs. We’ll chart a course that balances speed, dynamic content, and developer productivity, ensuring our web projects are ready for launch.

Prepare for liftoff! Here’s our mission roadmap:
· 🛰️ Mission Control: The Rendering Universe
The Cosmos of Web Rendering
Mission Control Briefing
· 🌏 Next.js: The All-Terrain Rover
Rendering Strategies & Capabilities
Project structure, Layout and Routing
Tools & Devtools
Optimization Boosters
Special Pages
Special Utilities
Multi-Zones
Mission Control Dashboard
· 🌔 Astro: The Lightweight Lunar Lander
Rendering Strategies & Capabilities
Project structure, Layout and Routing
Tools & Devtools
Optimization Boosters
Special Pages
Special Utilities
Mission Control Dashboard
· 🪐 Remix: The Data-Driven Starship
Rendering Strategies & Capabilities
Project structure, Layout and Routing
Tools & Devtools
Optimization Boosters
Special Pages, Components and Utilities
Mission Control Dashboard
· 📡 Voices from the Cosmos: Community and Performance Insights
Community Feedback: The Astronaut’s Perspective
Telemetry Data: Benchmarking the Spaceships
Final Countdown: Making the Decision
· 🪂 Mission Accomplished: Our Rendering Strategy Secured

Buckle up, space travelers! We’re about to blast off into the world of rendering! 🚀

🛰️ Mission Control: The Rendering Universe

The Cosmos of Web Rendering

In the vast universe of web development, rendering is the process of transforming code (HTML, CSS, JavaScript) into the visual representation of a web page that we see in our browsers. It’s the magic that takes place behind the scenes, allowing us to interact with websites, applications, and everything in between.

But just like the cosmos itself, the world of web rendering is vast and diverse, with multiple stars vying for our attention. The three main stars in this constellation are: Server-Side Rendering (SSR), Static Site Generation (SSG) and Incremental Static Regeneration (ISR).

1️⃣ With SSR, the server does the heavy lifting, generating the complete HTML page for every request. This ensures dynamic and personalized content but can be a bit slower than other options, especially under heavy traffic.

https://www.thetombomb.com/posts/rendering-options-on-the-web

2️⃣ With SSG, the HTML pages are generated at build time and served directly from a Content Delivery Network (CDN), resulting in blazing-fast load times and good performance.

https://www.thetombomb.com/posts/rendering-options-on-the-web

3️⃣ With ISR, pages are initially generated statically, but they can be dynamically updated in the background based on specific triggers or time intervals. This provides a balance of speed and dynamic content updates.

A diagram of the request flow for Incremental Static Regeneration

Choosing the right rendering strategy is crucial for our web projects’ success. Each approach has its own strengths and weaknesses, and the best choice will depend on our specific needs and priorities. The main key factors we need to consider are performance, SEO, dynamic content, scalability, and developer experience.

Understanding these rendering strategies is the first step in our journey. In the next section, we’ll meet the three “spaceships” that will help us navigate this cosmic landscape: Next.js, Astro, and Remix.

Mission Control Briefing

In the vast expanse of the web rendering universe, three powerful spaceships have emerged as leading contenders for our next mission. Each one boasts unique strengths, capabilities, and approaches to tackling the challenges of modern web development.

Before we dive into the technicalities, let’s introduce the stars of our rendering journey: Next.js, Astro, and Remix.

✔️ Next.js, a creation of Vercel, is the trusty all-terrain rover of the JavaScript world, capable of navigating a wide range of terrains, from simple static landing pages to complex, dynamic applications. Its hybrid rendering capabilities (SSR, SSG, ISR) make it a popular choice for projects that require both performance and flexibility.

✔️ Astro, a rising star in the JavaScript world, is quickly gaining traction for its lightweight design and laser focus on performance. Like a nimble lunar lander, Astro prioritizes speed and efficiency, making it an good choice for content-focused websites, blogs, and documentation sites.

✔️ Remix, an innovative full-stack framework, courageously goes where no framework has gone before, prioritizing seamless user experiences through efficient data handling. Like a powerful starship, it’s designed for ambitious missions, capable of handling complex data flows and user interactions with ease.

https://2023.stateofjs.com/en-US/libraries/meta-frameworks/#meta_frameworks_ratios

In the next section, we’ll strap into the cockpit of each of these spaceships and explore their rendering capabilities in greater detail. We’ll examine how they handle SSR, SSG, and ISR, comparing their performance, flexibility, and developer experience. By the end of this journey, we’ll be equipped to choose the perfect spaceship for our next web development mission.

🌏 Next.js: The All-Terrain Rover

Next.js, like a versatile rover exploring new planets, is equipped with a powerful engine and an array of tools to tackle diverse web development landscapes. Let’s delve into its rendering strategies, core architecture, and optimization features to understand its full potential, especially with the introduction of the App Router in Next.js 13.

Rendering Strategies & Capabilities

🔳 In the App Router, all components are Server Components by default, allowing them to execute directly on the server. This enables seamless data fetching within layouts, pages, and individual components.

// app/page.tsx (Server Component)
async function getData() {
const res = await fetch('https://api.example.com/...')
// The return value is *not* serialized
// You can return Date, Map, Set, etc.

if (!res.ok) {
// This will activate the closest `error.js` Error Boundary
throw new Error('Failed to fetch data')
}

return res.json()
}

export default async function Page() {
const data = await getData()

return <main></main>
}

🔵 Therefore, whenever possible, it is recommended to fetch data within Server Components to ensure direct access to backend data sources and to prevent sensitive environment variables from being exposed to the client.

https://vercel.com/blog/understanding-react-server-components
https://vercel.com/blog/understanding-react-server-components

🔳 When the application requires interactivity or direct manipulation of the browser or client-side state, Client Components step into the spotlight. These components are hydrated on the client side after the initial server render, which brings the UI to life.

'use client' // Marks this as a Client Component

import { useState } from 'react';

export default function LikeButton() {
const [likes, setLikes] = useState(0);

function handleClick() {
setLikes(likes + 1);
}

return <button onClick={handleClick}>Like ({likes})</button>;
}
https://nextjs.org/learn/react-foundations/server-and-client-components
https://nextjs.org/docs/app/building-your-application/rendering/client-components

🔳 Next.js leverages streaming with Server Components to send partial UI updates to the client as they become available. This allows for faster initial page loads and a more responsive user experience, especially with complex or data-intensive components.

// app/page.js (Server Component)
import { Suspense } from 'react';
import Comments from './comments.js'; // Client Component that might take time to load

export default function Page() {
return (
<div>
{/* ... other content ... */}
<Suspense fallback={<p>Loading comments...</p>}>
<Comments />
</Suspense>
</div>
)
}
https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming
https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming

🔳 Next.js 13’s data fetching utilizes fetch() and async/await within Server Components. Data is static by default (rendered at build time), but the fetch options allow for customization:

  • { next: { revalidate: ... } } for periodic or on-demand revalidation of static data.
  • { cache: 'no-store' } for dynamic, uncached data.
https://vercel.com/blog/nextjs-app-router-data-fetching#static-vs.-dynamic-data

Now, instead of using getServerSideProps() and getStaticProps(), all fetched data is static by default, meaning it's rendered at build time. — https://vercel.com/blog/nextjs-app-router-data-fetching#static-vs.-dynamic-data

🔳 A fully static example:

// project creation
/*
✔ Would you like to use TypeScript? … No
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … No
✔ Would you like to use `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to customize the default import alias (@/*)? … No
*/

// src/app/posts/page.js
import React from "react";
import styles from "../page.module.css";

async function getPosts() {
const res = await fetch("https://jsonplaceholder.typicode.com/posts");
return res.json();
}

const posts = await getPosts();

export default async function PostsPage() {
return (
<main className={styles.main}>
<h1>Posts archive</h1>
<ol>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ol>
</main>
);
}

// npm run build && npm run start
/*
Creating an optimized production build ...
✓ Compiled successfully
✓ Linting and checking validity of types
✓ Collecting page data
✓ Generating static pages (6/6)
✓ Collecting build traces
✓ Finalizing page optimization

Route (app) Size First Load JS
┌ ○ / 5.42 kB 92.4 kB
├ ○ /_not-found 871 B 87.9 kB
└ ○ /posts 137 B 87.2 kB
+ First Load JS shared by all 87 kB
├ chunks/23-ef3c75ca91144cad.js 31.5 kB
├ chunks/fd9d1056-2821b0f0cabcd8bd.js 53.6 kB
└ other shared chunks (total) 1.87 kB

○ (Static) prerendered as static content
*/

The main text above is prerendered as static content.

🔳 A mixed example (static and dynamic):

// src/app/posts/[id]/page.js
import React from "react";
import { notFound } from "next/navigation";
import styles from "../../page.module.css";

async function getPost(id) {
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts/${id}`,
);
if (!res.ok) {
return null;
}
return res.json();
}

export async function generateStaticParams() {
const res = await fetch("https://jsonplaceholder.typicode.com/posts");
const posts = await res.json();

return posts.map((post) => ({
id: post.id.toString(),
}));
}

export default async function PostPage({ params }) {
const post = await getPost(params.id);

if (!post) {
notFound();
}

return (
<main className={styles.main}>
<h1>{post.title}</h1>
<p>{post.body}</p>
</main>
);
}

// npm run build && npm run start
/*
Creating an optimized production build ...
✓ Compiled successfully
✓ Linting and checking validity of types
✓ Collecting page data
✓ Generating static pages (106/106)
✓ Collecting build traces
✓ Finalizing page optimization

Route (app) Size First Load JS
┌ ○ / 5.42 kB 92.4 kB
├ ○ /_not-found 871 B 87.9 kB
├ ○ /posts 338 B 87.4 kB
└ ● /posts/[id] 338 B 87.4 kB
├ /posts/1
├ /posts/2
├ /posts/3
└ [+97 more paths]
*/

The main thing to note this time is that 106 pages have been generated. At build time, an additional 100 posts are created, and page paths are pre-rendered according to external data.

🔳 It is also possible to statically generate a subset of pages at build time and the rest on demand:

export async function generateStaticParams() {
// Generate two pages at build time and the rest (3-100) on-demand
return [{ id: '1' }, { id: '2' }];
}

🔳 It’s also possible to use ISR (Incremental Server Rendering) with a little bit of modification:

// `app` directory

export const dynamicParams = true;

export async function generateStaticParams() {
return [...]
}

async function getPost(params) {
...
}

export default async function Post({ params }) {
const post = await getPost(params);

return ...
}

When dynamicParams is enabled (default behavior), any requested route segment not pre-generated by generateStaticParams is rendered on-demand and then cached.

The behavior for dynamic segments not included in generateStaticParams can be customized:

  • true (Default): On-demand generation and caching of missing segments.
  • false: A 404 error is returned for missing segments.

💡 When dynamicParams = true, the segment uses Streaming Server Rendering.

🔳 Partial Pre-Rendering (PPR) is an experimental feature introduced in Next.js 14. The core idea of PPR is to split a page into two parts:

  • A static shell that’s pre-rendered on the server at build time (or potentially on-demand). This shell contains the basic structure and layout of the page, along with any static or non-personalized content.
  • Dynamic “holes” or placeholders for Client Components that will be fetched and hydrated on the client-side after the initial page load. These components handle dynamic or personalized content.
https://www.youtube.com/watch?v=MTcPrTIBkpA
https://www.youtube.com/watch?v=MTcPrTIBkpA

PPR utilizes React’s Suspense component and Server Components to achieve this split. Server Components render the static shell, while Client Components fill in the dynamic holes using client-side JavaScript:

// app/page.js

import { Suspense } from 'react';
import DynamicComponent from './dynamic-component'; // Client Component

export default function Page() {
return (
<div>
{/* Static content rendered on the server */}
<h1>Welcome to My Page</h1>

<Suspense fallback={<p>Loading dynamic content...</p>}>
{/* Dynamic content rendered on the client */}
<DynamicComponent />
</Suspense>
</div>
);
}

ISR and PPR both optimize performance by blending static and dynamic content, but differ in approach. ISR pre-renders entire pages at build time (like SSG) and revalidates them in the background, while PPR selectively pre-renders specific components on the server, leaving others for client-side hydration.

https://www.youtube.com/watch?v=MTcPrTIBkpA

❌ As of August 2024, Partial Pre-Rendering (PPR) is still an experimental feature in Next.js.

🔳 Next.js prioritizes static rendering by default, optimizing for performance and SEO. However, it provides flexibility to introduce dynamic elements strategically, either through generateStaticParams alone (for pre-rendering all possible routes) or in combination with dynamicParams: true (for on-demand rendering of un-pre-rendered routes).

❌ Next.js, unlike Remix or Astro, doesn’t have a way to self-host using serverless. You can run it as a Node application. This however doesn’t work the same way as it does on Vercel. — https://www.epicweb.dev/why-i-wont-use-nextjs

With the App Router, the traditional boundaries between SSR, SSG, and ISR become increasingly blurred, as Next.js intelligently optimizes rendering based on the types of components (Client or Server), routing patterns ( [id] or [slug]), and data-fetching techniques (fetch , TanStack Query or SWR) used within the application.

Project structure, Layout and Routing

🔳 In Next.js, the project’s file structure directly dictates its routing behavior:

https://nextjs.org/docs/app/building-your-application/routing/defining-routes
https://nextjs.org/docs/app/building-your-application/routing/colocation#safe-colocation-by-default
https://nextjs.org/docs/app/building-your-application/routing/colocation#safe-colocation-by-default
https://nextjs.org/docs/app/building-your-application/routing/colocation#private-folders
https://nextjs.org/docs/app/building-your-application/routing/colocation#src-directory

🔳 The good news is that, except for routing folder and file conventions, Next.js is unopinionated about how we organize and colocate our project files. For instance, ekino-France adheres to a specific architecture known as HOFA. With Next.js, it’s feasible to implement the HOFA organization while still adhering to the framework’s routing conventions:

HOFA with Next.js (Image by the author)

Tools & Devtools

🔳 Next.js provides a command-line interface (CLI) and a project generator to streamline project setup and development workflows.

🔳 When using the Next.js project generator, many essential settings, such as ESLint configuration, TypeScript support, testing framework integration, and styling options, are generated by default.

What is your project named?  my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like to use `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to customize the default import alias (@/*)? No / Yes

🔳 Manual setup is indeed possible and beneficial when integrating Next.js into an existing project. With this flexibility, we can customize the configuration to match our project’s specific needs and existing structure.

🔳 The next/core-web-vitals plugin enhances ESLint's capabilities by elevating certain rules from warnings to errors if they negatively impact Core Web Vitals.

🔳 Next.js doesn’t enforce a specific state management library.

Optimization Boosters

Next.js offers a comprehensive suite of built-in components and optimizations designed to enhance application’s performance and Core Web Vitals.

🔵 Core Components:

  • Image: Automatic image optimization, resizing, and lazy loading for faster image delivery across devices.
  • Link: Prefetching capabilities for quicker page transitions and improved perceived performance.
  • Script: Granular control over script loading and execution to optimize JavaScript delivery.

🔵 Built-in Optimizations:

  • next/font: The app fonts (including custom fonts) will be automatically optimized and external network requests will be removed to improve privacy and performance.
  • next/dynamic: Enables dynamic imports for components, loading them only when needed to improve initial load times.

🔵 Additional Utilities:

  • @next/bundle-analyzer: Analyze the application's bundle size to identify optimization opportunities.
  • next/web-vitals: Measure and report on Core Web Vitals metrics, helping to track and improve performance.
  • @next/third-parties: Optimizes the loading and execution of third-party scripts, minimizing their impact on the application's performance.

Special Pages

Next.js employs various special files within its App Directory structure to provide essential functionalities and enhance developer control over the application’s behavior. Some of the most common ones:

  • layout.js: This file defines the shared layout or structure for multiple pages within a directory or nested route segment. It helps maintain consistency across the application and reduces code duplication.
  • page.js: The heart of each route, this file contains the main content and logic for a specific page in the application.
  • loading.js: Used to display a loading state while components or data are being fetched, improving the user experience during page transitions or data loading.
  • error.js: Handles errors that occur during rendering or data fetching, allowing for user-friendly error messages and graceful recovery from unexpected situations.
  • not-found.js: Defines the content to be displayed when a user navigates to a non-existent route (404 error).
  • template.js: (Experimental) Enables the definition of reusable UI templates for specific sections or layouts within the application.

The full list of available special pages can be found here.

Special Utilities

Beyond its core components and special pages, Next.js offers a collection of specialized utilities to facilitate navigation and data handling within the application.

  • useParams: Access route parameters for dynamic routes and make it possible to extract information from the URL.
  • usePathname: Retrieve the current pathname of the active route, useful for navigation logic or conditional rendering.
  • useRouter: Interact with the Next.js router, which helps with programming-based navigation between pages, accessing routing information, and managing route transitions.
  • useSearchParams: Retrieve and manage query parameters from the URL, enabling dynamic filtering, sorting, or other data-driven interactions.

The full list of available special utilities can be found in the official documentation.

Multi-Zones

An interesting feature offered by Next.js is Multi-Zones, an approach to micro-frontends. It allows a large application to be divided into smaller, independent Next.js applications, each serving a specific set of paths on the same domain. This modularization can improve build times and reduce code complexity.

https://nextjs.org/docs/app/building-your-application/deploying/multi-zones
  • Each zone is a standard Next.js application configured with a basePath to prevent conflicts.
/** @type {import('next').NextConfig} */
const nextConfig = {
basePath: '/blog',
}
  • Navigation within a zone is seamless (soft navigation), while navigating between zones triggers a full page reload (hard navigation).
  • An HTTP proxy or a designated Next.js application can be used to route requests to the correct zone.
  • Anchor tags (<a>) should be used for links between zones.
  • Code sharing can be facilitated through monorepos or NPM packages, and feature flags can help coordinate feature releases across zones.

Let’s go back to Mission Control, consolidate our findings, and assess Next.js’s overall capabilities.

Mission Control Dashboard

This table provides a summary of everything we have seen so far:

Next.js main feature (Image by the author)

Our journey through Next.js’s App Router has been enlightening. It’s clear that Next.js is a mature and well-designed framework, offering a comprehensive suite of features that make it a compelling choice for a wide range of web development missions.

If we didn’t know it was built on React, we might even mistake it for a complete programming language! It provides a seamless developer experience, complete with a rich toolkit of utilities, built-in intelligence for optimizing rendering strategies, and sophisticated caching and memoization mechanisms.

While Next.js is undeniably a strong contender, our curiosity compels us to explore other planets in this rendering universe. Next stop: Astro, the lightweight lunar lander!

🌔 Astro: The Lightweight Lunar Lander

Rendering Strategies & Capabilities

🔳 Astro inherently supports the Islands architecture:

https://www.patterns.dev/vanilla/islands-architecture/

The “Islands architecture” aims to minimize JavaScript shipped to the browser. It isolates interactive components (“islands”) within static HTML pages, reducing the need for client-side hydration and improving performance.

https://frontendatscale.com/blog/islands-architecture-state/

🔳 The default rendering mode is output:'static', which creates HTML for all page routes during build time. This means that the entire site will be pre-rendered and the server will have all pages built ahead of time and ready to be sent to the browser.

Statically rendering the index.astro page
https://astro.build/blog/hybrid-rendering/

The same HTML document is sent to the browser for every visitor, and a full site rebuild is required to update the contents of the page (as on SSG).

🔳 Every website generated by Astro includes zero client-side JavaScript. Components from frameworks like Vue, React, and Astro are automatically rendered to HTML during the build process, with all JavaScript removed.

🔳 JavaScript hydration is applied on a per-component basis:

// Load on page load
<MyComponent client:load />

// Load once the rest of page is done loading
<MyComponent client:idle />

// Load once component is visible in user's viewport
<MyComponent client:visible />

// Load whenever specific media query is met
<MyComponent client:media={QUERY} />

// Skip HTML rendering and client loads JavaScript
<MyComponent client:only={FRAMEWORK} />

🔳 By using an SSR adapter, two other output modes can be configured to render any or all of pages, routes, or API endpoints on demand:

  • output: 'server' for highly dynamic sites with most or all on-demand routes.
https://astro.build/blog/hybrid-rendering/
  • output: 'hybrid' for mostly static sites with some on-demand routes.
https://astro.build/blog/hybrid-rendering/
Static and server-side generated pages in the same project

🔳 These configurations should be added to a special Astro’s configuration file astro.config.mjs:

// 1. npx astro add node

import { defineConfig } from 'astro/config'

import node from '@astrojs/node'

// https://astro.build/config
export default defineConfig({
output: 'server',
adapter: node({
mode: 'standalone',
}),
})

❌ Astro’s adapters provide flexibility and extensibility but can add complexity to the developer experience. They’re great for diverse deployment needs and server-side integrations, but might require additional learning and configuration compared to frameworks with built-in features (like Next.js).

Project structure, Layout and Routing

🔳 A specific folder layout is enforced (opinionated) by Astro to ensure consistency. This includes:

  • src/*: The project's source code, including components, pages, styles, etc. is stored here.
  • public/*: Unprocessed assets like fonts and icons are placed in this directory.
  • package.json: This is the project's manifest file.
  • astro.config.mjs: An Astro configuration file is recommended.
  • tsconfig.json: A TypeScript configuration file is recommended.
https://thevalleyofcode.com/astro/2-astro-site-structure

🔳 Files within the src/pages directory hold special significance as they are recognized by Astro as routes and are served accordingly within the application.

https://thevalleyofcode.com/astro/4-adding-more-pages
https://github.com/understanding-astro/understanding-astro-book/blob/master/ch5.md
https://github.com/understanding-astro/understanding-astro-book/blob/master/ch5.md

🔳 In addition to the opinionated project structure, Astro component files are uniquely identified by both their .astro extension and their specific content structure:

---
// Your component script here!
import Banner from '../components/Banner.astro';
import ReactPokemonComponent from '../components/ReactPokemonComponent.jsx';
const myFavoritePokemon = [/* ... */];
const { title } = Astro.props;
---
<!-- HTML comments supported! -->
{/* JS comment syntax is also valid! */}

<Banner />
<h1>Hello, world!</h1>

<!-- Use props and other variables from the component script: -->
<p>{title}</p>

<!-- Include other UI framework components with a `client:` directive to hydrate: -->
<ReactPokemonComponent client:visible />

<!-- Mix HTML with JavaScript expressions, similar to JSX: -->
<ul>
{myFavoritePokemon.map((data) => <li>{data.name}</li>)}
</ul>

<!-- Use a template directive to build class names from multiple strings or even objects! -->
<p class:list={["add", "dynamic", {classNames: true}]} />
https://thevalleyofcode.com/astro/2-astro-site-structure
https://dev.to/omher/what-is-astrojs-500c

❌ Astro does not have direct built-in support for React Server Components (RSCs) in the same way that Next.js does, but in on-demand rendered modes, Astro components essentially function as SSR components, providing dynamic capabilities.

❌ Developers may need to invest time familiarizing themselves with Astro’s structure and component model.

🔳 To use React components within Astro, an adapter is required, along with updates to the astro.config.mjs file. The same for other UI frameworks.

https://github.com/understanding-astro/understanding-astro-book/blob/master/ch5.md
https://dev.to/omher/what-is-astrojs-500c

❌ While the adapter abstraction can offer benefits in certain scenarios, it might introduce potential challenges when dealing with updates to Astro or integrated frameworks.

🔳 Astro utilizes a custom compiler (transform .astro to valid TypeScript), written in Go and distributed as WASM, rather than relying on widely-known compilers like Turbo or Vite.

❌ While Astro’s custom compiler may yield performance gains, its less widespread adoption can lead to challenges in community support and debugging, especially when encountering new or specific errors.

🔳 It’s possible to use Tailwind with Astro, always via an adapter.

https://thevalleyofcode.com/astro/9-css
https://thevalleyofcode.com/astro/9-css

More information about CSS and styling can be found here.

🔳 Like Next.js, Astro supports both static (src/pages/about.astro -> mysite.com/about) and dynamic (src/pages/authors/[author].astro) routing. Astro also allows multiple levels of dynamic segments, like /[category]/[post].

Tools & Devtools

🔳 Like Next.js, Astro provides a rich command-line interface (CLI) to streamline project setup and development workflows.

https://thevalleyofcode.com/astro/1-first-astro-site
https://thevalleyofcode.com/astro/1-first-astro-site

🔳 Manual setup is indeed possible and beneficial when integrating Astro into an existing project.

🔳 Key Astro CLI commands:

  • astro preview: Launches a local development server for real-time previews.
  • astro check: Runs diagnostics (such as type-checking within .astro files) against the project and reports errors to the console.
  • astro sync: Generates TypeScript types for all Astro modules.
  • astro preferences: Manage development settings (devToolbar, checkUpdates, etc).
  • astro telemetry: Sets telemetry configuration to collect anonymous telemetry data about general usage

🔳 Commonly used tools such as eslint-plugin-astro, prettier-plugin-astro, and stylelint can also be integrated into Astro projects.

🔳 Astro supports both unit and end-to-end testing through integration with popular testing frameworks. Vitest can be configured for unit tests, while Playwright, Cypress, or Nightwatch can be used for end-to-end tests.

🔳 Astro features a built-in development toolbar during local previews, equipped with tools for debugging and inspecting the site, including islands, performance, and accessibility issues.

Optimization Boosters

🔳 By default, any <script> tags within an Astro component are processed and bundled together, then injected into the <head> of the HTML with type="module". This means they are deferred and only execute after the DOM is fully loaded.

🔴 is:inline directive on the <script> tag bypasses Astro's optimizations and can impact performance.

🔳 Built-in <Image /> Component: Astro's image component provides automatic optimization, resizing, and lazy loading, ensuring optimal image delivery across different devices and network conditions.

---
import { Image } from 'astro:assets';
import localBirdImage from '../../images/subfolder/localBirdImage.png';
---
<Image src={localBirdImage} alt="A bird sitting on a nest of eggs." />
<Image src="/images/bird-in-public-folder.jpg" alt="A bird." width="50" height="50" />
<Image src="https://example.com/remote-bird.jpg" alt="A bird." width="50" height="50" />

<img src={localBirdImage.src} alt="A bird sitting on a nest of eggs.">
<img src="/images/bird-in-public-folder.jpg" alt="A bird.">
<img src="https://example.com/remote-bird.jpg" alt="A bird.">

🔳 Astro’s <Picture /> component handles scenarios where different image formats or sizes are needed based on device capabilities.

---
import { Picture } from 'astro:assets';
import myImage from '../assets/my_image.png'; // Image is 1600x900
---

<!-- `alt` is mandatory on the Picture component -->
<Picture src={myImage} formats={['avif', 'webp']} alt="A description of my image." />


<!-- Output -->
<picture>
<source srcset="/_astro/my_image.hash.avif" type="image/avif" />
<source srcset="/_astro/my_image.hash.webp" type="image/webp" />
<img
src="/_astro/my_image.hash.png"
width="1600"
height="900"
decoding="async"
loading="lazy"
alt="A description of my image."
/>
</picture>

🔳 <ViewTransitions />: This component, available in both static and on-demand rendered modes, enables smooth animations and state preservation between page navigations.

---
import { ViewTransitions } from 'astro:transitions';
---
<html lang="en">
<head>
<title>My Homepage</title>
<ViewTransitions />
</head>
<body>
<h1>Welcome to my website!</h1>
</body>
</html>

🔳 In Astro, CSS rules defined within <style> tags are automatically scoped by default. This means the styles are encapsulated within the component and only apply to the HTML generated within that component. This automatic scoping helps prevent style conflicts and promotes maintainability within larger projects.

<style>
h1 {
color: red;
}

.text {
color: blue;
}
</style>

<!-- Compile to -->
<style>
h1[data-astro-cid-hhnqfkh6] {
color: red;
}

.text[data-astro-cid-hhnqfkh6] {
color: blue;
}
</style>

Special Pages

Astro provides a way to customize the pages displayed when errors occur:

🔳 404 Page (Not Found):

  • For this, we should create a 404.astro or 404.md file in the src/pages directory (src/pages/404.astro) to customize the page shown when a user tries to access a non-existent route.
<html lang="en">
<head>
<title>Not found</title>
</head>
<body>
<p>Sorry, this page does not exist.</p>
<img src="https://http.cat/404" />
</body>
</html>

🔳 500 Page (Internal Server Error):

  • For this, we should create a 500.astro file in the src/pages directory (src/pages/500.astro) to customize the page displayed for errors that occur during on-demand rendering (SSR).
  • Note: This custom 500 page won’t be used for pre-rendered (SSG) pages.
---
interface Props {
error: unknown
}

const { error } = Astro.props
---

<div>{error instanceof Error ? error.message : 'Unknown error'}</div>

Special Utilities

🔵 Astro offers a variety of special utilities:

🔳 Astro global: Available in all .astro files.

🔳 astro:content: Provides APIs to configure and query your Markdown or MDX documents in src/content/.

🔳 astro:middleware : Enables interception and modification of requests and responses.

🔳 astro:i18n: Provides functions to create URLs using your project’s configured locales.

🔵 Furthermore, Astro provides a variety of helpful directives:

🔳 Common Directives:

  • class:list: Converts an array to a class string.
  • set:html: Injects raw HTML (use with caution).
  • set:text: Injects escaped text.

🔳 Client Directives: Control UI component hydration:

  • client:load, client:idle, client:visible, client:media, client:only

🔳 Script & Style Directives

  • is:global: Applies styles globally.
  • is:inline: Prevents Astro from processing the tag.
  • define:vars: Passes server-side variables to client-side code.

🔳 Advanced:

  • is:raw: Treats children as plain text.

Mission Control Dashboard

Houston, we’ve completed our reconnaissance of Astro! Here’s the mission report:

Astro prioritizes performance and innovation, but its unique approach can result in a steeper learning curve and potential maintenance challenges compared to more traditional frameworks like Next.js. Consider Astro for performance-critical, content-focused projects; otherwise, Next.js may offer a smoother experience, especially if your project relies on cutting-edge React features or a rapidly evolving ecosystem.

Buckle up, space explorers! We’re leaving the moon behind and venturing into the Remix galaxy, where data reigns supreme. 🚀

🪐 Remix: The Data-Driven Starship

Rendering Strategies & Capabilities

🔳 Remix, a full-stack JavaScript web framework built on React, handles both frontend and backend responsibilities:

  • On the frontend, it acts as a higher level React framework that offers server-side rendering, file-based routing, nested routes and more.
  • On the backend, it serves the frontend and manages data requests.
https://www.mattstobbs.com/introduction-to-remix/
https://codilime.com/blog/project-in-remix-best-practices/

🔳 Remix was acquired by Shopify.

🔳 Remix distinguishes itself with features tailored for SSR optimization:

  • Intelligent client hydration: Client-side JavaScript is hydrated only when necessary, leading to faster and more efficient user experiences.
  • Incremental hydration: JavaScript is downloaded and executed progressively as users interact with the site, improving responsiveness.
  • Intelligent caching: Advanced caching techniques ensure rapid content delivery, even on slower connections.

❌ Remix doesn’t support SSG in the traditional sense, but we can provide a similar experience using HTTP caching and CDNs.

Instead of prescribing a precise architecture with all of its constraints like SSG, Remix is designed to encourage you to leverage the performance characteristics of distributed computing. — https://remix.run/docs/en/main/guides/performance

Project structure, Layout and Routing

🔳 A typical structure for organizing a Remix project might look like this:

Remix project organization (Image by the author)
Remix project organization (Image by the author)
  • app/entry.client.tsx: This file serves as the entry point for the browser, responsible for hydrating the server-rendered markup, making the application interactive.
import { RemixBrowser } from "@remix-run/react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";

startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser />
</StrictMode>
);
});
  • app/entry.server.tsx: This file’s default export is a function that generates the initial server response, providing complete control over the HTTP status, headers, and HTML sent to the client.
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
const ifNoneMatch = request.headers.get("if-none-match");
const etag = responseHeaders.get("etag");

if (ifNoneMatch !== null && etag !== null && etag === ifNoneMatch) {
return new Response(null, { status: 304, headers: responseHeaders });
}

return isbot(request.headers.get("user-agent"))
? handleBotRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
)
: handleBrowserRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
);
}
  • app/root.tsx: defines the sole mandatory route in a Remix application, serving as the parent to all routes within the routes/ directory. It's responsible for rendering the foundational <html> document, within which all other routes are rendered via the <Outlet /> component.
root.tsx (Image by the author)
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";

export default function Root() {
return (
<html lang="en">
<head>
<Links />
<Meta />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
  • app/routes: JavaScript or TypeScript files within the app/routes directory are automatically transformed into routes within the Remix application. The filename directly corresponds to the route's URL pathname, with the exception of _index.tsx, which serves as the index route for the root route.
app/
├── routes/
│ ├── _index.tsx
│ └── about.tsx
└── root.tsx

/ ---> app/routes/_index.tsx
/about ---> app/routes/about.tsx

🔳 Adding a . to a route filename will create a / in the URL:

app/
├── routes/
│ ├── _index.tsx
│ ├── about.tsx
│ ├── concerts.trending.tsx
│ ├── concerts.salt-lake-city.tsx
│ └── concerts.san-diego.tsx
└── root.tsx

/ ---> app/routes/_index.tsx
/about ---> app/routes/about.tsx
/concerts/trending ---> app/routes/concerts.trending.tsx
/concerts/salt-lake-city ---> app/routes/concerts.salt-lake-city.tsx
/concerts/san-diego ---> app/routes/concerts.san-diego.tsx

🔳 Adding a $ to a route filename will create a dynamic segments:

 app/
├── routes/
│ ├── _index.tsx
│ ├── about.tsx
│ ├── concerts.$city.tsx
│ └── concerts.trending.tsx
└── root.tsx



/ ---> app/routes/_index.tsx
/about ---> app/routes/about.tsx
/concerts/trending ---> app/routes/concerts.trending.tsx
/concerts/salt-lake-city ---> app/routes/concerts.$city.tsx
/concerts/san-diego ---> app/routes/concerts.$city.tsx

✳️ Remix will parse the value from the URL and pass it to various APIs:

export async function loader({
params,
}: LoaderFunctionArgs) {
return fake.db.getConcerts({
date: params.date,
city: params.city,
});
}

🔳 Remix maintains UI synchronization with persistent server state through a three-step process:

https://remix.run/docs/en/main/discussion/data-flow

1️⃣ Data is provided to the UI through route loaders:

// routes/account.tsx

import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno

export async function loader({
request,
}: LoaderFunctionArgs) {
const user = await getUser(request);
return json({
displayName: user.displayName,
email: user.email,
});
}

export default function Component() {
// ...
}

export async function action() {
// ...
}

✳️ We can consider Remix loaders as a type of Backend-for-Frontend (BFF) pattern:

Remix loader (Image by the author)

2️⃣ Forms submit data to route actions, which update the persistent state:

// routes/account.tsx

import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno

export async function loader({
request,
}: LoaderFunctionArgs) {
const user = await getUser(request);
return json({
displayName: user.displayName,
email: user.email,
});
}

export default function Component() {
// ...
}

export async function action({
request,
}: ActionFunctionArgs) {
const formData = await request.formData();
const user = await getUser(request);

await updateUser(user.id, {
email: formData.get("email"),
displayName: formData.get("displayName"),
});

return json({ ok: true });
}

3️⃣ Loader data on the page is automatically revalidated to reflect the updated state:

// routes/account.tsx

import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno

export async function loader({
request,
}: LoaderFunctionArgs) {
const user = await getUser(request);
return json({
displayName: user.displayName,
email: user.email,
});
}

export default function Component() {
const user = useLoaderData<typeof loader>();
return (
<Form method="post" action="/account">
<h1>Settings for {user.displayName}</h1>

<input
name="displayName"
defaultValue={user.displayName}
/>
<input name="email" defaultValue={user.email} />

<button type="submit">Save</button>
</Form>
);
}

export async function action({
request,
}: ActionFunctionArgs) {
const formData = await request.formData();
const user = await getUser(request);

await updateUser(user.id, {
email: formData.get("email"),
displayName: formData.get("displayName"),
});

return json({ ok: true });
}

✳️ The unidirectional data flow enforced in Remix simplifies the application’s structure, making it more intuitive and easier to understand and reason about.

https://remix.run/blog/remix-data-flow
https://remix.run/blog/remix-data-flow

✳️ Remix runs the app on both the server and the browser, with distinct code execution for each environment. During the build step:

  • A server build is created, bundling everything into a single (or multiple) module(s).
  • A client build is created, splitting the app into multiple bundles for optimized browser loading.
  • Server-specific code (action, headers, loader, and dependencies) is removed from the client bundles.

🔳 It’s possible to explicitly separate server and client code in Remix:

  • While not strictly necessary, .server modules provide a way to clearly mark entire modules as server-only. The build process is designed to fail if any code from a .server file or directory is inadvertently included in the client module graph.
app
├── .server 👈 marks all files in this directory as server-only
│ ├── auth.ts
│ └── db.ts
├── cms.server.ts 👈 marks this file as server-only
├── root.tsx
└── routes
└── _index.tsx
  • Conversely, the contents of modules can be excluded from the server build by appending *.client.ts to the filename or nesting them within a .client directory.
import { supportsVibrationAPI } from "./feature-check.client.ts";

console.log(supportsVibrationAPI);
// server: undefined
// client: true | false

✳️ Remix is built on top of React Router and maintained by the same team, enabling the utilization of all React Router features within Remix applications.

Tools & Devtools

🔳 Project creation in Remix is streamlined through the create-remix command-line tool.

Initiating a Remix project (Image by the author)

A collection of Remix templates and stacks can be found here.

🔳 Remix leverages Vite for development server functionality (remix vite:dev) and production builds (remix vite:build):

Starting a Remix project (Image by the author)

🔳 @remix-run/testing : this package offers utilities for unit testing Remix applications. This is achieved through mocking Remix route modules and the generation of an in-memory React Router app, allowing for testing of UI components using preferred testing stacks (Jest, Vitest, RTL, etc.).

// An example of testing using jest and React Testing Library

import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { createRemixStub } from "@remix-run/testing";
import {
render,
screen,
waitFor,
} from "@testing-library/react";

test("renders loader data", async () => {
function MyComponent() {
const data = useLoaderData() as { message: string };
return <p>Message: {data.message}</p>;
}

const RemixStub = createRemixStub([
{
path: "/",
Component: MyComponent,
loader() {
return json({ message: "hello" });
},
},
]);

render(<RemixStub />);

await waitFor(() => screen.findByText("Message: hello"));
});

🔳 A variety of additional community-developed tools for Remix can be found here.

Optimization Boosters

🔳 Server-side rendering (SSR) in Remix allows for parallel execution of tasks (HTML generation, asset loading, data fetching), potentially leading to faster initial loads than traditional Single Page Applications (SPAs) where everything waits for JavaScript.

// SPA
HTML |---|
JavaScript |---------|
Data |---------------|
page rendered 👆


// Remix
👇 first byte
HTML |---|-----------|
JavaScript |---------|
Data |---------------|
page rendered 👆

🔳 Remix builds on HTML, ensuring basic features work even with JavaScript disabled, improving accessibility and providing a baseline experience for all users.

🔳 Remix emphasizes the importance of network-aware UI feedback, providing visual cues during network-intensive actions to create a positive user experience. Three primary mechanisms are employed:

  • Busy Indicators: Visual cues displayed while an action is processed on the server, used when the outcome is unpredictable.
  • Optimistic UI: Immediately updates the UI with the expected state before receiving the server’s response, enhancing perceived speed and responsiveness.
  • Skeleton Fallbacks: Visual placeholders displayed during initial loading or when non-critical data is being fetched, improving the user’s perception of progress.
https://nebulab.com/blog/shopify-hydrogen-v2-updates

🔳 Browser-Inspired Behavior

  • Link Navigation: Similar to browser behavior, Remix prioritizes the most recent navigation request, canceling any pending requests from prior clicks.
  • Form Submission: Mirroring browsers, Remix handles only the latest form submission, discarding earlier submissions if a new one is initiated before the first completes.

🔳 Remix also allows for enhancing user experience by streaming content as it becomes available, rather than waiting for the entire page to load.

🔳 Remix simplifies state management by automatically handling network-related state and providing natural data storage mechanisms, reducing the need for client-side state and traditional React patterns. This results in cleaner code, improved data freshness, and fewer synchronization issues.

✳️ In most cases, we won’t need additional libraries like TanStack Query, Apollo, or Redux for state management in Remix.

https://www.epicweb.dev/the-webs-next-transition

Special Pages, Components and Utilities

🔳 Remix provides a set of specialized components that play key roles in building web applications:

  • Await: Used within Suspense boundaries to handle asynchronous data fetching in components, gracefully displaying loading states or fallback UI while data is being fetched.
  • Form: A powerful component that streamlines form handling and data mutations, integrating seamlessly with Remix's actions and loaders.
  • Link: The core component for creating navigational links within the Remix application. It optimizes navigation by prefetching data for linked routes, resulting in faster page transitions.
  • Scripts: Manages the loading and execution of JavaScript scripts in the Remix application, allowing for script placement control and optimization of loading behavior.
  • ScrollRestoration: Preserves the user's scroll position during navigation, ensuring a smooth and seamless browsing experience.

🔳 Remix offers an array of helpful hooks that simplify data fetching, navigation, state management, and more. Some of the key players:

  • useLoaderData: Access the data loaded by a route's loader function, ensuring fresh data for every request.
  • useActionData: Retrieve data returned from a route's action function, ideal for handling form submissions and mutations.
  • useFetcher: Provides a convenient way to trigger and manage data fetching and mutations from anywhere in the application, enabling optimistic UI updates and progress indicators.
  • useSearchParams: Retrieve and manage query parameters from the URL, enabling dynamic filtering, sorting, or other data-driven interactions.
  • useSubmit: A convenient hook for programmatically submitting forms and triggering actions, simplifying form handling and interactions

🔳 Remix extends its core functionality with a range of utilities and APIs:

  • cookies: Provides a convenient way to manage cookies in the Remix application, including creation, reading, updating, and deletion.
  • defer: Used in loaders to defer the loading of non-critical data, enabling streaming and progressive rendering.
  • Sessions: Provides a simple API for managing user sessions in Remix.
  • redirect: Redirects the user to a different route, either within the application or to an external URL.

Mission Control Dashboard

After our deep dive into the Remix Starship, let’s return to Mission Control and review our findings on this data-driven vessel:

We’ve completed our solo flights through the rendering universe. It’s time to connect with fellow space explorers and gather their insights. Let’s see what the community has to say about Next.js, Astro, and Remix, and consolidate our findings into a final mission report.

📡 Voices from the Cosmos: Community and Performance Insights

Community Feedback: The Astronaut’s Perspective

To help us visualize the capabilities of these rendering spacecraft, let’s consult this star chart:

https://developer.chrome.com/blog/frameworks-may-2024

✔️ Our mission debrief aligns with the latest telemetry from Mission Control at Chrome: Next.js remains a versatile all-terrain rover, Astro excels in its lightweight performance, and Remix shines in data-driven navigation.

✔️ Prominent developer Kent C. Dodds favors Remix over Next.js, citing its alignment with web standards, deployment flexibility, and code simplicity. He expresses concerns about Next.js’s abstractions, perceived complexity, stability, and potential vendor lock-in. In his view, Remix fosters more maintainable applications and transferable web development skills.

✔️ Based on the Stack Overflow Developer Survey 2024, here are some additional community insights about the frameworks we’re considering:

  • Next.js’s Lead: Next.js holds a strong position with 17.9% adoption, indicating its widespread use and popularity among developers.
  • Astro’s Growth: While Astro is relatively new, it has garnered 3% adoption, suggesting growing interest and adoption within the community.
  • Remix’s Niche: Remix, at 1.6% adoption, appears to be a more niche framework compared to Next.js and Astro.
https://survey.stackoverflow.co/2024/technology#most-popular-technologies-webframe

✔️ More community insights conveyed by the “Meta-Frameworks Ratios Over Time” from the State of JS 2023 survey:

https://2023.stateofjs.com/en-US/libraries/meta-frameworks/
  • Next.js clearly leads the pack in terms of usage (56%), awareness (98%), and positivity (64%).
  • Astro has seen the most rapid growth in interest (62%) in recent years.
  • Remix, although newer, shows a steady increase in usage (from 4% to 10%) and awareness (from 55% to 79%). However a decrease in interest (from 68% to 48%).

✔️ The sentiment analysis reveals a positive trend towards most of the meta-frameworks, with Next.js leading in terms of user satisfaction. Astro also demonstrates a strong positive sentiment, while Gatsby and Remix show lower “Would use again” rates and higher negative sentiment.

https://2023.stateofjs.com/en-US/libraries/meta-frameworks/#meta_frameworks_experience
https://2023.stateofjs.com/en-US/libraries/meta-frameworks/#meta_frameworks_experience

✔️ Some negative feedback regarding ‘Remix’:

https://2023.stateofjs.com/en-US/libraries/meta-frameworks/#meta_frameworks_experience

✔️ The download trends for @remix-run/react, astro, and next :

https://npmtrends.com/@remix-run/react-vs-astro-vs-next

Telemetry Data: Benchmarking the Spaceships

Here are some essential insights from the Astro 2023 Web Framework Performance Report:

✔️ First Input Delay (FID): All frameworks perform well, showcasing good responsiveness to initial user interactions:

https://astro.build/blog/2023-web-framework-performance-report/

✔️ Cumulative Layout Shift (CLS): All frameworks scored 50% or higher in this metric. However, it’s the youngest frameworks (Astro, SvelteKit, and Remix) that score the highest on this metric:

https://astro.build/blog/2023-web-framework-performance-report/

✔️ Largest Contentful Paint (LCP): Astro and SvelteKit lead in LCP, indicating faster loading of main content compared to other frameworks:

https://astro.build/blog/2023-web-framework-performance-report/

The moment of truth has arrived. We’ve explored the vastness of the rendering universe, and now, back at Mission Control, we must make the critical decision: which spaceship will lead us to the stars?

Final Countdown: Making the Decision

After careful deliberation and considering both our technical analysis and the valuable feedback from the web development community, we’ve made our decision: Next.js will be the meta-framework propelling our future web development missions at ekino-France.

Why Next.js?

  • It offers a balanced approach to rendering, prioritizing static performance while seamlessly integrating dynamic capabilities (SSR and ISR) when needed.
  • Its mature ecosystem, extensive community, and well-documented resources facilitate smoother onboarding and a wealth of support for developers.
  • Next.js’s automatic optimizations and intelligent rendering decisions streamline development and ensure exceptional performance for a wide range of project types, from content-heavy sites to complex applications.

While mastering its advanced features might involve a learning curve, and some overhead might be present for smaller projects, Next.js’s overall versatility, performance, and developer-friendly experience make it the ideal choice for Ekino’s diverse web development needs.

We’re confident that Next.js will empower us to build fast, scalable, and user-friendly web applications that will reach new heights in the ever-evolving digital landscape.

With our mission objectives in sight and our spacecraft selected, it’s time to finalize our flight plan and prepare for launch. 🚀

🪂 Mission Accomplished: Our Rendering Strategy Secured

Houston, we have a solution! We’ve successfully navigated the vast expanse of the web rendering universe, carefully examining the strengths and weaknesses of Next.js, Astro, and Remix. Through meticulous analysis and a deep dive into their rendering capabilities, we’ve selected Next.js as the optimal spacecraft to power our web development missions at ekino-France.

When SSR/SSG isn’t required, our go-to stack is React, powered by Vite for development and Vitest for testing.

With our rendering strategy secured, the next phase of our mission involves integrating Next.js into our Bistro code generator, further expanding its capabilities.

Thank you for joining us on this exhilarating journey through the web rendering cosmos! We hope this guide has equipped you with the knowledge and insights to confidently choose the right rendering strategy and framework for your next project. Remember, the best tool for the job depends on your specific needs and priorities.

As we continue our exploration of the ever-expanding web development universe, we’re excited to share more insights and adventures with you.

Until then, happy coding! ❤️

Want to Connect? 
You can find us at GitHub: https://github.com/ekino

--

--

Héla Ben Khalfallah
ekino-france

Hi! I'm Hela Ben Khalfallah, Senior Frontend Expert specializing in JavaScript, React, Node.js, software architecture, and FrontendOps.