vibecheck0.3.0
Performancedom-bloaterror

Excessive DOM size in Next.js

Every DOM node the browser has to style, lay out, and keep in memory costs time on each frame, so a page with thousands of nodes feels sluggish to scroll and slow to interact with. AI agents produce this constantly: they map over data without virtualization, wrap everything in defensive <div>s, and render hidden content with display:none instead of not rendering it. The result passes review because it looks right — it just quietly janks.

Symptoms

  • Scrolling stutters and interactions feel laggy on lower-end devices
  • React/Vue DevTools shows thousands of elements under one list
  • Style recalculation and layout dominate the Performance panel flame chart
  • Memory climbs with the size of the rendered list

How VibeCheck catches it

In your widget · Problems

errordom-bloatDOM has 1500 nodes

To your coding agent · MCP

agent › get_detected_issues
{ detector: "dom-bloat", issue: "DOM has 1500 nodes", threshold: "≥ 800 nodes (warning), ≥ 1,500 nodes (error) — sampled every 5s" }

The same string in your widget and in your agent’s context — no screenshot, no copy-paste.

Root causes

  • Rendering a long list in full instead of windowing/virtualizing it
  • Deeply nested wrapper <div>s that add nodes without structure
  • Hidden content rendered with display:none instead of not being mounted
  • Duplicate renders of the same subtree

The fix for Next.js

Same virtualization applies, but also move list rendering into Server Components and stream it — the client bundle and hydration cost drop, and you can paginate at the data layer instead of shipping every row.

Paginate on the server instead of rendering all rowstsx
export default async function Page({ searchParams }: { searchParams: Promise<{ page?: string }> }) {
  const { page = '1' } = await searchParams
  const rows = await db.items.findMany({ take: 50, skip: (Number(page) - 1) * 50 })
  return <List rows={rows} />
}

Steps

  1. Find the largest list or repeated subtree (VibeCheck reports the heaviest selector)
  2. Virtualize it so only visible rows are in the DOM
  3. Remove redundant wrapper elements and replace display:none with conditional rendering

See the general, framework-agnostic fix →

FAQ

How many DOM nodes is too many?
Lighthouse warns past ~800 and errors past ~1,500 nodes in one document — the same thresholds VibeCheck uses. Under ~800 is comfortable; the exact number matters less than avoiding unbounded lists.
Does hiding elements with display:none reduce DOM size?
No. Hidden elements are still in the DOM and still cost memory and (for some operations) layout. Conditionally render them instead so they aren’t created until needed.
Will content-visibility fix it on its own?
It skips rendering work for offscreen sections, which is a big, cheap win — but the nodes still exist. For truly large lists (thousands of rows) you still need virtualization.