Litro vs Next.js
Both frameworks solve the same core problems — SSR, file-based routing, TypeScript, deployment adapters. The difference is the component model and what you're betting on long-term: React's ecosystem today, or web platform standards for the next decade.
Feature Comparison
| Feature | Next.js | Litro |
|---|---|---|
| Component model | React (JSX) | Lit (web components) |
| File-based routing | ✓ | ✓ |
| SSR | React Server Components | Declarative Shadow DOM streaming |
| SSG | ✓ | ✓ |
| Data fetching | fetch() in Server Components | definePageData() |
| API routes | app/api/route.ts | server/api/*.ts (H3) |
| Client routing | Next Router | LitroRouter (URLPattern) |
| Hello World JS (gzipped) | ~90 kB | ~8 kB |
| Server engine | Custom | Nitro (same as Nuxt) |
| Virtual DOM | ✓ | — |
| W3C standard components | — | ✓ |
| TypeScript | ✓ | ✓ |
| License | MIT | Apache 2.0 |
Side by Side: A Simple Page
Next.js — app/about/page.tsx
export default function AboutPage() {
return (
<main>
<h1>About</h1>
<p>We build web frameworks.</p>
</main>
);
}
Litro — pages/about.ts
import { html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { LitroPage } from '@beatzball/litro/runtime';
@customElement('page-about')
export class AboutPage extends LitroPage {
override render() {
return html`
<main>
<h1>About</h1>
<p>We build web frameworks.</p>
</main>
`;
}
}
export default AboutPage;
Side by Side: Data Fetching
Next.js — Server Component
// app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: { slug: string };
}) {
const post = await db.getPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML=
{{ __html: post.body }} />
</article>
);
}
Litro — definePageData()
// pages/blog/[slug].ts
export const pageData = definePageData(
async (event) => {
const slug = event.context.params?.slug;
const post = await db.getPost(slug);
return { post };
}
);
@customElement('page-blog-slug')
export class BlogPost extends LitroPage {
override render() {
const { post } = this.serverData ?? {};
if (!post) return html`<p>Loading…</p>`;
return html`
<article>
<h1>${post.title}</h1>
${unsafeHTML(post.body)}
</article>
`;
}
}
What Carries Over
- File-based routing — same
pages/convention, same dynamic segment syntax ([slug],[...all]) - API routes — same concept, different handler format (H3 instead of Web Request/Response)
- TypeScript throughout — Litro is TypeScript-first
- Deployment adapters — Litro uses Nitro, which has Vercel, Cloudflare Workers, AWS Lambda, and more as first-party presets
- SSG — export
generateRoutes()instead ofgenerateStaticParams()
What Changes
- React → Lit — function components become class components; JSX becomes tagged template literals
- No React runtime — the client bundle drops from ~90 kB to ~8 kB for Hello World
- Shadow DOM instead of CSS Modules — component styles are scoped at the browser level
- H3 API routes — import
defineEventHandlerfromh3rather than using Next's Web Request/Response format - Server engine — Nitro instead of Next's custom server (more deployment targets, same Vercel support)
Why Build on Web Standards?
Litro's component model is built on Custom Elements, Shadow DOM, and <slot> — W3C
specifications implemented natively in every major browser. Because they're part of the
web platform itself, Lit components interoperate with any framework, work in plain HTML,
and will continue to be supported for as long as browsers exist.
For new projects, building on web standards means your component layer stays stable as the broader ecosystem evolves. There's no framework version to upgrade, no breaking changes to absorb — the browser's implementation is the spec.
Read more: Why Web Components? The case for standards-based development
Litro