Files
0156307f55 [Bench] Extend bench app to have realistic client chunk counts (#95814)
Follow-up to https://github.com/vercel/next.js/pull/95807.

Makes payload size match prod sizes closer and aligns chunk count by
adding a lot of client modules.

I originally thought to unify them into `client.js` or such but maybe
it's good to see how compilation behaves with many small files? Which is
how real projects do that.

```
=== dashboard.html (fixture)  vs  p-overview.html (real)
  total KB                            665          637
  rows (model/I/T/other)      258/111/3/1 240/125/5/27
  byte share model/I/T %          43/57/0      36/62/2
  median row size B                   906          106
  max depth                            43           53
  mean depth                          8.9         10.5
  elements per KB(model)              7.7          5.2
  pure-data byte share %               18           21
  client-ref elements %                35           32
  objects:elements ratio             1.18         1.23
  avg props per element               2.6          3.1
  median children fanout                4            4
  string bytes %                       50           54
  median string len B                  12           15
  row refs per KB                     0.4          0.4
  encoded scalars per KB              0.7          1.2
  undefined markers                   247          740

=== docs.html (fixture)  vs  nextjs-docs.html (real)
  total KB                            487          487
  rows (model/I/T/other)        72/40/7/1   55/43/2/20
  byte share model/I/T %           89/8/3       94/6/0
  median row size B                   199          340
  max depth                            27           32
  mean depth                         22.3         12.3
  elements per KB(model)              0.6          0.8
  pure-data byte share %                7            9
  client-ref elements %                29           33
  objects:elements ratio             8.67         4.92
  avg props per element               2.0          2.8
  median children fanout                3            4
  string bytes %                       47           64
  median string len B                  17           35
  row refs per KB                     0.2          0.1
  encoded scalars per KB              7.4          2.9
  undefined markers                  3586         1377

=== blog.html (fixture)  vs  vercel-blog.html (real)
  total KB                            778          797
  rows (model/I/T/other)        72/39/0/1  102/53/7/14
  byte share model/I/T %           93/7/0       87/8/5
  median row size B                   145          505
  max depth                            34           44
  mean depth                          9.6         12.2
  elements per KB(model)              0.7          1.1
  pure-data byte share %               92           85
  client-ref elements %                38           33
  objects:elements ratio            31.19        16.39
  avg props per element               1.9          2.7
  median children fanout                3            4
  string bytes %                       46           48
  median string len B                   6            9
  row refs per KB                     0.1          0.1
  encoded scalars per KB              0.4          0.6
  undefined markers                   242          426
```

Co-authored-by: Claude Fable 5 <[email protected]>
2026-07-15 15:13:35 +02:00

233 lines
8.0 KiB
JavaScript

// Models the marketing/blog payload profile: cards are server-rendered
// markup (dense spans and list items whose leaf props are preservable),
// with small client atoms per card, structured-data JSON-LD, and repeated
// utility class strings — the composition production blog indexes show.
import '../bench.css'
import Avatar from '../ui/avatar'
import TagPill from '../ui/tag-pill'
import PostGrid from '../ui/post-grid'
import SavePost from '../ui/save-post'
import MegaNav from '../ui/mega-nav'
import BlogSearch from '../ui/blog-search'
import ThemeToggle from '../ui/theme-toggle'
import ReadingProgress from '../ui/reading-progress'
import CategoryFilter from '../ui/category-filter'
import CarouselControls from '../ui/carousel-controls'
import LikeButton from '../ui/like-button'
import ShareMenu from '../ui/share-menu'
import NewsletterForm from '../ui/newsletter-form'
import LoadMore from '../ui/load-more'
import VideoEmbed from '../ui/video-embed'
import PodcastTeaser from '../ui/podcast-teaser'
import LocalePicker from '../ui/locale-picker'
import IconXSocial from '../ui/icons/x-social'
import IconLinkedin from '../ui/icons/linkedin'
import IconYoutube from '../ui/icons/youtube'
import IconRss from '../ui/icons/rss'
import IconHeart from '../ui/icons/heart'
import IconMail from '../ui/icons/mail'
import IconCalendar from '../ui/icons/calendar'
import IconLink from '../ui/icons/link'
import IconShare from '../ui/icons/share'
import IconPlay from '../ui/icons/play'
import IconSparkles from '../ui/icons/sparkles'
import IconTrendingUp from '../ui/icons/trending-up'
import IconHeadphones from '../ui/icons/headphones'
import IconFilter from '../ui/icons/filter'
import { posts, categories } from '../lib/data'
export const dynamic = 'force-dynamic'
function Cover({ cover }) {
return (
<div
className="cover"
role="img"
aria-label={cover.alt}
style={{
background: `linear-gradient(135deg, hsl(${cover.hueA} 70% 55%), hsl(${cover.hueB} 70% 40%))`,
}}
/>
)
}
// The first page of cards is server-rendered for instant first paint;
// the client grid below also receives the data for filtering, like
// production blog indexes do.
function ServerPostCard({ post }) {
return (
<article className="post-card" data-post={post.slug}>
<Cover cover={post.cover} />
<div className="post-body">
<div className="post-tags flex items-center gap-2 truncate">
{post.tags.map((t) => (
<TagPill key={t} tag={t} />
))}
</div>
<h3 className="truncate text-sm font-medium">{post.title}</h3>
<p className="text-sm text-muted">{post.excerpt}</p>
<div className="post-meta flex items-center gap-2 truncate text-xs text-muted">
<Avatar
name={post.author.name}
hue={post.author.avatarHue}
size={20}
title={post.author.title}
/>
<span className="truncate">{post.author.name}</span>
<span>·</span>
<IconCalendar size={12} />
<time dateTime={post.publishedAt}>{post.publishedAt}</time>
<span>·</span>
<span>{post.readingMinutes} min</span>
{post.stats.views > 60000 ? (
<span className="trending-pill">
<IconTrendingUp size={11} /> Trending
</span>
) : null}
<span className="header-spacer" />
<LikeButton
slug={post.slug}
count={post.stats.likes}
icon={<IconHeart size={13} />}
/>
<ShareMenu
slug={post.slug}
trigger={<IconShare size={13} />}
items={[
{ label: 'Share on X', icon: <IconXSocial size={12} /> },
{ label: 'Share on LinkedIn', icon: <IconLinkedin size={12} /> },
{ label: 'Copy link', icon: <IconLink size={12} /> },
]}
/>
<SavePost slug={post.slug} />
</div>
</div>
</article>
)
}
function jsonLd(post) {
return JSON.stringify({
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
description: post.seo.description,
author: { '@type': 'Person', name: post.author.name },
datePublished: post.publishedAt,
url: post.seo.canonical,
image: post.seo.ogImage,
})
}
export default function BlogPage() {
const [featured, ...rest] = posts
return (
<>
<ReadingProgress />
<header className="app-header">
<nav className="crumbs" aria-label="Breadcrumb">
<strong>Acme</strong>
<span className="sep">/</span>
<span className="current">Blog</span>
</nav>
<MegaNav />
<div className="header-spacer" />
<BlogSearch placeholder="Search posts…" />
<ThemeToggle />
</header>
<main className="blog-main">
<CategoryFilter
categories={Object.values(categories)}
icon={<IconFilter size={13} />}
/>
<section className="blog-hero">
<Cover cover={featured.cover} />
<div>
<div className="post-tags flex items-center gap-2">
{featured.tags.map((t) => (
<TagPill key={t} tag={t} />
))}
</div>
<h1>{featured.title}</h1>
<p className="text-sm text-muted">{featured.excerpt}</p>
<div className="post-meta flex items-center gap-2 text-xs text-muted">
<Avatar
name={featured.author.name}
hue={featured.author.avatarHue}
size={24}
/>
<span>
{featured.author.name} · {featured.author.role}
</span>
<span>
· {featured.publishedAt} · {featured.readingMinutes} min read
</span>
</div>
</div>
</section>
<div className="section-head flex items-center gap-2">
<h2 className="more-posts">Latest posts</h2>
<span className="header-spacer" />
<CarouselControls pages={4} label="Browse latest posts" />
</div>
<section className="post-grid" aria-label="Latest posts">
{rest.slice(0, 12).map((post) => (
<ServerPostCard key={post.id} post={post} />
))}
</section>
<section className="media-row" aria-label="Watch and listen">
<VideoEmbed
title="Keynote: the state of the framework"
duration="42:18"
hue={262}
playIcon={<IconPlay size={22} />}
/>
<PodcastTeaser
title="Shipping the App Router migration"
episode={48}
icon={<IconHeadphones size={18} />}
/>
<div className="newsletter-box">
<h3>
<IconSparkles size={14} /> What's new, monthly
</h3>
<p className="text-sm text-muted">
One email a month with the best posts and release notes.
</p>
<NewsletterForm icon={<IconMail size={14} />} />
</div>
</section>
<h2 className="more-posts">All posts</h2>
<PostGrid posts={rest} />
<LoadMore total={rest.length} pageSize={24} />
{posts.slice(0, 6).map((post) => (
<script
key={post.id}
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: jsonLd(post) }}
/>
))}
<footer className="blog-footer flex items-center gap-2 text-xs text-muted">
<span>© Acme Inc.</span>
<a href="#" aria-label="Acme on X">
<IconXSocial size={14} />
</a>
<a href="#" aria-label="Acme on YouTube">
<IconYoutube size={14} />
</a>
<a href="#" aria-label="Acme on LinkedIn">
<IconLinkedin size={14} />
</a>
<a href="#" aria-label="RSS feed">
<IconRss size={14} />
</a>
<span className="header-spacer" />
<LocalePicker />
</footer>
</main>
</>
)
}