Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .github/pr-assets/benchmark-desktop.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions app/(home)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { Hero } from "../components/Hero";
import { Benchmark } from "../components/Benchmark";
import { Features } from "../components/Features";
import { Playground } from "../components/Playground";
import { Footer } from "../components/Footer";
Expand All @@ -9,6 +10,7 @@ export default function Home() {
return (
<main className="min-h-screen bg-background">
<Hero />
<Benchmark />
<Features />
<Playground />
<Footer />
Expand Down
247 changes: 247 additions & 0 deletions app/components/Benchmark.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
"use client";

import { motion } from "framer-motion";
import { ArrowUpRight, Gauge, Rocket, RefreshCcw, Trophy } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { useI18n } from "../i18n/context";

const benchmarkCases = [
{
caseName: "react-1k",
modules: "1,000 components",
startup: { tool: "Rsbuild", value: "1529ms" },
build: { tool: "Vite (Rolldown)", value: "628ms" },
hmr: { tool: "Rspack CLI", value: "107ms" },
},
{
caseName: "react-5k",
modules: "5,000 components",
startup: { tool: "Rsbuild", value: "1346ms" },
build: { tool: "Vite (Rolldown)", value: "1921ms" },
hmr: { tool: "Farm", value: "85ms" },
},
{
caseName: "react-10k",
modules: "10,000 components",
Comment on lines +12 to +26
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The modules field contains hardcoded English strings (e.g., "1,000 components"). These will not be translated when the site is switched to Chinese. Consider moving these strings to the translation files or using a translation key to ensure the entire section is properly localized.

startup: { tool: "Rsbuild", value: "1188ms" },
Comment on lines +11 to +27
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The per-case "modules" label is hardcoded in English (e.g., "1,000 components"), so the Chinese homepage will still show English copy. If the goal is fully localized benchmark content, consider moving this label/unit into i18n (or formatting the count with Intl.NumberFormat and translating the unit).

Copilot uses AI. Check for mistakes.
build: { tool: "Vite (Rolldown)", value: "3283ms" },
hmr: { tool: "Vite (Rolldown)", value: "94ms" },
},
] as const;

const benchmarkHighlights = [
{
key: "startup",
icon: Rocket,
accent: "from-sky-500/25 via-cyan-500/10 to-transparent",
tool: "Rsbuild",
value: "1188ms",
},
{
key: "build",
icon: Gauge,
accent: "from-emerald-500/25 via-lime-500/10 to-transparent",
tool: "Vite (Rolldown)",
value: "628ms",
},
{
key: "hmr",
icon: RefreshCcw,
accent: "from-amber-500/25 via-orange-500/10 to-transparent",
tool: "Farm",
value: "85ms",
},
] as const;

export function Benchmark() {
const { t } = useI18n();

return (
<section id="benchmark" className="relative px-4 py-24 md:py-32 overflow-hidden">
<div className="absolute inset-0 -z-10">
<div className="absolute inset-x-0 top-12 h-64 bg-[radial-gradient(circle_at_top,rgba(56,189,248,0.12),transparent_55%)]" />
<div className="absolute inset-x-0 bottom-0 h-72 bg-[radial-gradient(circle_at_center,rgba(234,88,12,0.09),transparent_55%)]" />
</div>

<div className="max-w-[1600px] mx-auto px-6 md:px-12">
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.3 }}
transition={{ duration: 0.6 }}
className="max-w-3xl"
>
<div className="inline-flex items-center gap-2 rounded-full border border-sky-400/20 bg-sky-400/8 px-4 py-1.5 text-xs font-semibold uppercase tracking-[0.24em] text-sky-300">
<Trophy className="h-3.5 w-3.5" />
<span>{t.benchmark.badge}</span>
</div>
<h2 className="mt-6 text-4xl font-black tracking-[-0.04em] text-foreground md:text-6xl">
{t.benchmark.title}
</h2>
<p className="mt-5 max-w-2xl text-lg leading-8 text-muted-foreground/80 md:text-xl">
{t.benchmark.description}
</p>
</motion.div>

<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.2 }}
transition={{ duration: 0.6, delay: 0.08 }}
className="mt-10 grid gap-4 lg:grid-cols-[1.3fr_0.7fr]"
>
<Card className="glass-card border-white/10 bg-transparent">
<CardContent className="flex flex-col gap-6 p-6 md:flex-row md:items-end md:justify-between">
<div>
<p className="text-xs uppercase tracking-[0.24em] text-muted-foreground/60">
{t.benchmark.sourceLabel}
</p>
<p className="mt-2 text-lg font-semibold text-foreground">
{t.benchmark.sourceText}
</p>
</div>
<div>
<p className="text-xs uppercase tracking-[0.24em] text-muted-foreground/60">
{t.benchmark.casesLabel}
</p>
<div className="mt-3 flex flex-wrap gap-2">
{benchmarkCases.map((item) => (
<span
key={item.caseName}
className="rounded-full border border-white/10 bg-white/5 px-3 py-1 text-sm text-foreground/80"
>
{item.caseName}
</span>
))}
</div>
</div>
</CardContent>
</Card>

<div className="flex items-stretch">
<Button
size="lg"
variant="outline"
className="h-auto w-full justify-between rounded-[1.75rem] border-white/10 bg-white/5 px-6 py-5 text-left text-base text-foreground hover:bg-white/10"
onClick={() =>
window.open(
"https://github.com/utooland/build-tools-performance",
"_blank",
)
}
>
<span>{t.benchmark.cta}</span>
<ArrowUpRight className="h-5 w-5 shrink-0" />
Comment on lines +124 to +135
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CTA uses window.open with target "_blank" but without disabling access to window.opener, which enables reverse-tabnabbing. Prefer rendering the Button as an anchor (Button asChild + ) with rel="noopener noreferrer", or pass a third window.open feature string including "noopener,noreferrer".

Suggested change
size="lg"
variant="outline"
className="h-auto w-full justify-between rounded-[1.75rem] border-white/10 bg-white/5 px-6 py-5 text-left text-base text-foreground hover:bg-white/10"
onClick={() =>
window.open(
"https://github.com/utooland/build-tools-performance",
"_blank",
)
}
>
<span>{t.benchmark.cta}</span>
<ArrowUpRight className="h-5 w-5 shrink-0" />
asChild
size="lg"
variant="outline"
className="h-auto w-full rounded-[1.75rem] border-white/10 bg-white/5 text-base text-foreground hover:bg-white/10"
>
<a
href="https://github.com/utooland/build-tools-performance"
target="_blank"
rel="noopener noreferrer"
className="flex w-full items-center justify-between px-6 py-5 text-left"
>
<span>{t.benchmark.cta}</span>
<ArrowUpRight className="h-5 w-5 shrink-0" />
</a>

Copilot uses AI. Check for mistakes.
</Button>
Comment on lines +123 to +136
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using onClick with window.open for an external link is not ideal for accessibility and SEO. It is better to use a standard anchor tag (<a>). Since you are using a UI Button component, you can use the asChild pattern to render it as a link while preserving the button's styles.

            <Button
              asChild
              size="lg"
              variant="outline"
              className="h-auto w-full justify-between rounded-[1.75rem] border-white/10 bg-white/5 px-6 py-5 text-left text-base text-foreground hover:bg-white/10"
            >
              <a
                href="/utooland/build-tools-performance"
                target="_blank"
                rel="noopener noreferrer"
              >
                <span>{t.benchmark.cta}</span>
                <ArrowUpRight className="h-5 w-5 shrink-0" />
              </a>
            </Button>

</div>
</motion.div>

<div className="mt-12 grid gap-5 lg:grid-cols-3">
{benchmarkHighlights.map((item, index) => {
const Icon = item.icon;
const copy = t.benchmark.highlights[item.key];

return (
<motion.div
key={item.key}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.2 }}
transition={{ duration: 0.5, delay: index * 0.08 }}
>
<Card className="glass-card relative h-full overflow-hidden border-white/10 bg-transparent">
<div className={`absolute inset-0 bg-gradient-to-br ${item.accent}`} />
<CardHeader className="relative z-10">
<div className="flex items-center justify-between">
<div className="rounded-2xl border border-white/10 bg-white/8 p-3">
<Icon className="h-5 w-5 text-foreground" />
</div>
<span className="rounded-full bg-black/20 px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-white/75">
{item.tool}
</span>
</div>
<CardTitle className="pt-6 text-3xl font-black tracking-[-0.03em] md:text-4xl">
{item.value}
</CardTitle>
</CardHeader>
<CardContent className="relative z-10 space-y-2">
<h3 className="text-lg font-semibold text-foreground">{copy.title}</h3>
<p className="text-sm leading-7 text-muted-foreground/80">
{copy.description}
</p>
</CardContent>
</Card>
</motion.div>
);
})}
</div>

<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.2 }}
transition={{ duration: 0.6, delay: 0.12 }}
className="mt-12"
>
<Card className="glass-card overflow-hidden border-white/10 bg-transparent">
<CardHeader className="border-b border-white/10">
<CardTitle className="text-2xl font-bold">{t.benchmark.tableTitle}</CardTitle>
<p className="text-sm leading-7 text-muted-foreground/75">
{t.benchmark.tableDescription}
</p>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full min-w-[760px] text-left">
<thead className="bg-white/5">
<tr className="text-sm uppercase tracking-[0.2em] text-muted-foreground/60">
<th className="px-6 py-4 font-medium">{t.benchmark.columns.case}</th>
<th className="px-6 py-4 font-medium">{t.benchmark.columns.startup}</th>
<th className="px-6 py-4 font-medium">{t.benchmark.columns.build}</th>
<th className="px-6 py-4 font-medium">{t.benchmark.columns.hmr}</th>
Comment on lines +189 to +202
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For table accessibility, the header cells should declare scope="col" (and optionally add a or aria-describedby pointing at the CardTitle/description) so screen readers can correctly associate headers with cells.

Suggested change
<CardTitle className="text-2xl font-bold">{t.benchmark.tableTitle}</CardTitle>
<p className="text-sm leading-7 text-muted-foreground/75">
{t.benchmark.tableDescription}
</p>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full min-w-[760px] text-left">
<thead className="bg-white/5">
<tr className="text-sm uppercase tracking-[0.2em] text-muted-foreground/60">
<th className="px-6 py-4 font-medium">{t.benchmark.columns.case}</th>
<th className="px-6 py-4 font-medium">{t.benchmark.columns.startup}</th>
<th className="px-6 py-4 font-medium">{t.benchmark.columns.build}</th>
<th className="px-6 py-4 font-medium">{t.benchmark.columns.hmr}</th>
<CardTitle id="benchmark-table-title" className="text-2xl font-bold">
{t.benchmark.tableTitle}
</CardTitle>
<p
id="benchmark-table-description"
className="text-sm leading-7 text-muted-foreground/75"
>
{t.benchmark.tableDescription}
</p>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<table
aria-labelledby="benchmark-table-title"
aria-describedby="benchmark-table-description"
className="w-full min-w-[760px] text-left"
>
<thead className="bg-white/5">
<tr className="text-sm uppercase tracking-[0.2em] text-muted-foreground/60">
<th scope="col" className="px-6 py-4 font-medium">
{t.benchmark.columns.case}
</th>
<th scope="col" className="px-6 py-4 font-medium">
{t.benchmark.columns.startup}
</th>
<th scope="col" className="px-6 py-4 font-medium">
{t.benchmark.columns.build}
</th>
<th scope="col" className="px-6 py-4 font-medium">
{t.benchmark.columns.hmr}
</th>

Copilot uses AI. Check for mistakes.
</tr>
</thead>
<tbody>
{benchmarkCases.map((item) => (
<tr key={item.caseName} className="border-t border-white/10">
<td className="px-6 py-5">
<div className="font-semibold text-foreground">{item.caseName}</div>
<div className="mt-1 text-sm text-muted-foreground/70">
{item.modules}
</div>
</td>
<td className="px-6 py-5">
<Metric tool={item.startup.tool} value={item.startup.value} />
</td>
<td className="px-6 py-5">
<Metric tool={item.build.tool} value={item.build.value} />
</td>
<td className="px-6 py-5">
<Metric tool={item.hmr.tool} value={item.hmr.value} />
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
</motion.div>

<p className="mt-5 text-sm leading-7 text-muted-foreground/65">
{t.benchmark.note}
</p>
</div>
</section>
);
}

function Metric({ tool, value }: { tool: string; value: string }) {
return (
<div className="space-y-1">
<div className="text-base font-semibold text-foreground">{value}</div>
<div className="text-sm text-muted-foreground/70">{tool}</div>
</div>
);
}
76 changes: 76 additions & 0 deletions app/i18n/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,44 @@ export const translations = {
preview: "Preview",
},
},
benchmark: {
badge: "Build Tools Performance",
title: "Benchmarks from real-world workloads",
description:
"Results pulled from utooland/build-tools-performance and distilled into the metrics most teams actually compare: dev startup, production build speed, and HMR latency.",
sourceLabel: "Source",
sourceText: "GitHub Actions run on November 13, 2025",
casesLabel: "Covered workloads",
summaryTitle: "Where the leaders stand out",
summaryDescription:
"A quick read on the best cold-start, build, and HMR results across the benchmark suite.",
Comment on lines +200 to +202
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The translation keys summaryTitle and summaryDescription are defined but never used in the Benchmark component. If they were intended to be used as a header for the highlights section, please add them to the JSX; otherwise, they should be removed to avoid cluttering the translation files.

Comment on lines +200 to +202
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new benchmark translations include summaryTitle/summaryDescription, but they don't appear to be used by the Benchmark component. Either render these strings in the section UI or remove them to avoid unused translation surface area.

Suggested change
summaryTitle: "Where the leaders stand out",
summaryDescription:
"A quick read on the best cold-start, build, and HMR results across the benchmark suite.",

Copilot uses AI. Check for mistakes.
tableTitle: "Fastest result by case",
tableDescription:
"Each row highlights the best result recorded for that workload in the upstream benchmark report.",
columns: {
case: "Case",
startup: "Fastest startup",
build: "Fastest build",
hmr: "Fastest HMR",
},
highlights: {
startup: {
title: "Fastest dev startup",
description: "Rsbuild leads cold boot time on every React workload shown here.",
},
build: {
title: "Fastest production build",
description: "Vite with Rolldown takes the shortest cold build path in the React cases.",
},
hmr: {
title: "Lowest HMR latency",
description: "Sub-100ms refresh remains achievable even as the app size scales up.",
},
},
note:
"Benchmarks run on shared GitHub Actions hardware, so absolute timings vary. The section focuses on relative winners from the published report.",
cta: "View full benchmark repo",
},
// Footer
footer: {
builtWith: "Built with",
Expand Down Expand Up @@ -390,6 +428,44 @@ export const translations = {
preview: "预览",
},
},
benchmark: {
badge: "Build Tools Performance",
title: "来自真实工作负载的 benchmark",
description:
"数据来自 utooland/build-tools-performance,并提炼成团队最常对比的指标:开发启动、生产构建速度,以及 HMR 延迟。",
sourceLabel: "数据来源",
sourceText: "GitHub Actions 运行记录,时间为 2025 年 11 月 13 日",
casesLabel: "覆盖场景",
summaryTitle: "领先结果一眼看清",
summaryDescription:
"快速展示 benchmark 套件里冷启动、生产构建和 HMR 的最佳成绩。",
Comment on lines +439 to +441
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The zh benchmark translations include summaryTitle/summaryDescription, but the Benchmark component doesn't appear to render them. Either wire them into the UI or remove them to avoid carrying unused translation keys.

Suggested change
summaryTitle: "领先结果一眼看清",
summaryDescription:
"快速展示 benchmark 套件里冷启动、生产构建和 HMR 的最佳成绩。",

Copilot uses AI. Check for mistakes.
tableTitle: "各场景最快结果",
tableDescription:
"每一行都标出上游 benchmark 报告中该工作负载的最快结果。",
columns: {
case: "场景",
startup: "最快启动",
build: "最快构建",
hmr: "最快 HMR",
},
highlights: {
startup: {
title: "开发冷启动最快",
description: "在这里展示的 React 场景中,Rsbuild 的冷启动都拿到第一。",
},
build: {
title: "生产构建最快",
description: "在 React 场景里,Vite + Rolldown 拿到了最短的冷构建时间。",
},
hmr: {
title: "HMR 延迟最低",
description: "即使项目规模继续放大,也能保持低于 100ms 的热更新速度。",
},
},
note:
"Benchmark 运行在共享的 GitHub Actions 机器上,绝对耗时会有波动。这里重点展示公开报告里的相对领先结果。",
cta: "查看完整 benchmark 仓库",
},
// Footer
footer: {
builtWith: "使用",
Expand Down