React DevTools Shows 24 of This Site's 92 Components
Open the Components tab on an App Router page and the tree looks empty. 68 of 92 components ran at build time and never mount in a browser.
Install React Developer Tools, open the Components tab on this site's home page, and you will find about eight entries under the root. The page is built from 92 components across five layers. The gap is not a bug in the extension: 68 of those 92 are Server Components that ran during next build and have no browser-side instance to select. In an App Router codebase, the Components tab stops being a picture of your UI and becomes a picture of your client bundle.
The short answer
React Developer Tools is a browser extension that adds two panels to DevTools: Components, an inspectable tree of mounted React components with live props and state, and Profiler, a commit-by-commit render recorder. It is available for Chrome, Firefox and Edge, and for React Native and non-browser targets via the standalone react-devtools package.
What it can see, layer by layer
Every component in this repository is one of five layers, and "use client" appears in exactly 24 of the 92 files:
| Layer | Components | Client ("use client") | Visible in the Components tab |
|---|---|---|---|
| Atoms | 7 | 0 | none |
| Molecules | 41 | 19 | 19 |
| Organisms | 33 | 5 | 5 |
| Templates | 11 | 0 | none |
| Total | 92 | 24 | 24 (26%) |
Two more files outside src/components carry the directive — src/app/error.tsx and src/app/global-error.tsx, which must be Client Components because error boundaries are a client-side concept — plus two hooks, src/lib/useOwnedProducts.ts and src/lib/useWishlist.ts.
The distribution is the interesting part. Atoms and templates are 100% server: an atom is a styled primitive with no state, and a template is a layout that orders organisms. Molecules carry 19 of the 24, because a molecule is where a single unit of interaction lives — a modal, a picker, a form, a toggle. The rule that produces that distribution is not "is this interactive?" but "what does this file drag across the boundary with it?"
What actually mounts on the home page
HomeTemplate composes ten organisms. Nine of them — Hero, FeaturedTemplates, TechStack, Features, FeatureTabs, UseCases, Testimonials, Blog, Footer — are Server Components. Their output is HTML, generated once for each of this build's 584 prerendered pages.
So the Components tree on / is:
Header, the only client organism on the page (it owns the mobile-menu open state)- six
TemplateCards, one per featured product, each nestingWishlistButtonandPreviewModal Analytics, Vercel's client component from@vercel/analytics/next
That is the whole list. Features renders fourteen feature cards from a typed data array and not one of them is selectable, because by the time the browser has the page, Features has been HTML for hours.
This is worth sitting with the first time you see it, because the instinct is to think the extension failed to attach. It did attach. It is telling you the truth about what you shipped.
Reading the tree as a bundle map
The useful reframing: the Components tab is your client boundary, rendered. Anything in that tree is JavaScript a visitor downloaded, parsed and hydrated. Anything absent cost them HTML only.
That makes it a review tool as much as a debugging one. Two findings in this codebase came out of exactly this reading:
A card that should not have been six components deep. The "More templates" rail that closes every product page originally rendered TemplateCard — the same client molecule as above, carrying a preview modal, a wishlist button and an ownership lookup. On a navigation rail, none of that is used. It now renders RelatedTemplateCard, a Server Component, so six cards' worth of interactive machinery stopped shipping on 111 product pages.
An auth stack on pages with no account UI. Header renders site-wide and useOwnedProducts renders behind every templates grid; both imported the Supabase browser client at module scope. That put @supabase/ssr plus auth-js — about 68 KiB over the wire, 255 KiB parsed — into the initial bundle of /, /blog, /docs and /pricing. The fix is in src/lib/supabase/lazyClient.ts: both call sites only touch the client inside an effect, so the import waits for the effect too, and a cookie probe skips it entirely for signed-out visitors.
// src/lib/supabase/lazyClient.ts
export function hasAuthCookie(): boolean {
if (typeof document === "undefined") return false;
return /(?:^|;\s*)sb-.+?-auth-token/.test(document.cookie);
}
export async function loadSupabaseClient(): Promise<BrowserClient> {
const { createClient } = await import("@/lib/supabase/client");
return createClient();
}
Neither of those is something the Components tab reports as a warning. Both are obvious the moment you look at a tree and ask why a component is in it.
The Profiler on an island
The Profiler records commits — each render React flushed to the DOM — with per-component timings. On a page of Server Components, that recording is scoped to the islands: click "record", open PreviewModal, stop, and you get the commits PreviewModal and its parent TemplateCard produced, not a whole-page render.
That constraint is mostly good news. A slow commit inside an island is genuinely a client-render problem, and the Profiler will name it. A slow page usually is not, and the Profiler will have nothing to say about it — which is itself the answer.
What to use for the other 68 components
| Question | Right tool | Not React DevTools, because |
|---|---|---|
| Why is this page slow to load? | Lighthouse / the Network panel | Load time here is HTML, images and the client bundle — none of it a React commit |
| Why is this component re-rendering? | Profiler → "Why did this render?" (enable it in Profiler settings) | Only applies to the 24 that render in the browser at all |
| What did a Server Component output? | View source, or the prerendered HTML in .next/server/app | The component instance no longer exists anywhere |
| What is in the client bundle? | next build --experimental-analyze — the webpack-era @next/bundle-analyzer plugin writes nothing under Turbopack | The Components tab shows what mounted, not how many bytes it cost |
| Is this component server or client? | grep -rl '"use client"' src | Faster and complete; the tab only shows what the current page mounted |
| Why did a Server Component throw? | The terminal running next dev, or the server logs | The error happened in Node, before any browser was involved |
Installing it, and the standalone fallback
The browser extension is the normal path — install for Chrome, Firefox or Edge and two tabs appear in DevTools, Components ⚛ and Profiler ⚛. The tabs only appear on pages that actually run React; a fully static route with no client components may show them greyed out or missing, which again is information rather than a fault.
When the extension cannot reach the page — a non-browser target, a WebView, an iframe with a restrictive policy — the standalone app is the fallback:
npx react-devtools
That opens a window and prints a <script src="http://localhost:8097"> tag to add to your document before any React code runs. Worth knowing this site would need a CSP exception for it: next.config.ts sets script-src 'self' 'unsafe-inline' plus three named hosts, and localhost:8097 is not one of them.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Components/Profiler tabs never appear | The page runs no client-side React, or the extension has no host permission on that origin | Check a page with a known client component; confirm the extension is enabled for the site in the browser's extension settings |
| The tree is far shallower than the page | Most components are Server Components — expected in the App Router | Read it as a client-boundary map, not a UI tree |
Component names are all t, o, n in production | Minification renames functions; there is no displayName to fall back on | Debug against next dev or a non-minified build; do not add display names just to read a production tree |
| "Why did this render?" is greyed out | The setting is off, or the recording predates it | Profiler → settings gear → "Record why each component rendered while profiling", then record again |
| Editing a prop in the panel does nothing visible | The value is re-derived on the next render from server-supplied props | Server-supplied props are not client state; change the source, not the panel |
| Profiler records zero commits on an interaction | The interaction changed no client state — it was a link, a form post, or a server round-trip | Look at the Network panel instead; nothing re-rendered in the browser |
| Extension shows a different React version than your app | Another React instance on the page (an embedded iframe, a browser extension of your own) | Check for duplicate React copies; the panel inspects whichever renderer registered first |
Frequently asked questions
Does React DevTools work with the Next.js App Router? Yes, for everything that runs in the browser. Client Components appear in the tree with live props and state, and the Profiler records their commits. Server Components do not appear as selectable, inspectable components, because they executed on the server and never mounted client-side — there is no instance holding state for the panel to read.
Why does the Components tab look almost empty on my page?
Usually because the page is mostly Server Components, which is the App Router working as intended. Check with grep -rl '"use client"' src: if the count is low, an empty-looking tree is the correct result. In this repo it is 24 files out of 92.
Is it safe to leave the extension installed while browsing normally?
Yes. It injects a hook that detects React and does nothing on pages without it. It does surface component names and props to anyone with DevTools open, but so does the page's own JavaScript — never treat client-side props as a place to keep a secret. That rule holds regardless of the extension: in this codebase the real gates run server-side, in authorizeDownload and in the database's row-level security policies.
Can I profile a production build?
You can, and you should when measuring — but names are minified. React ships a profiling build of react-dom for this; in practice, profile against next dev for readable names, then confirm the fix with Lighthouse against next build && next start, where the numbers are real.
Templates in this post
ASoc Pip, ASoc Press and ASoc Quest are Next.js + Tailwind landing page templates with the same shallow client boundary — the interactive pieces are isolated molecules, so what you inspect is a short list rather than the whole page.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates.
