DEV Community

Dhruvil Chauhan
Dhruvil Chauhan

Posted on

I built a CLI that tells you why your Next.js components became client components

React Server Components are great — until "use client" starts spreading through your codebase like a virus.

You added it to one file months ago. Now half your component tree is running on the client and you have no
idea which import dragged it there. Your bundle is massive, your Lighthouse score is suffering, and nobody
can explain why.

I built client-creep to solve this.

## What it does

  npx client-creep
Enter fullscreen mode Exit fullscreen mode

Zero setup. No install. Runs on any Next.js 13/14/15/16 project.

It answers three questions no existing tool answered together:

  1. Why is this a client component? — which import chain caused it
  2. Did it need to be? — or is it client purely by accident, with zero hooks or browser APIs?
  3. What is it costing you? — estimated KB being shipped to the browser

## Real output on a 534-file Next.js app

  ────────────────────────────────────────────────────────────
    client-creep  Next.js client component analysis
  ────────────────────────────────────────────────────────────

    Files scanned:            534
    Client components:        418  (182 boundaries)
    Estimated client JS:      2.29 MB
    Potentially recoverable:  237.4 KB  (113 creep candidates)

  ────────────────────────────────────────────────────────────
    ⚠  Accidental Client Creep
  ────────────────────────────────────────────────────────────

    ⚠ src/app/chat-insights/components/EmptyStates.tsx  19.7 KB recoverable
      No hooks, event handlers, or browser APIs detected
      Why client:
      ⚡ src/app/chat-insights/page.tsx ← use client
        └─ src/app/chat-insights/components/index.ts
             └─ src/app/chat-insights/components/EmptyStates.tsx
Enter fullscreen mode Exit fullscreen mode

That last section is the key one — accidental creep candidates. Components that are only client because
something upstream imported them through a barrel file. No hooks. No browser APIs. Just an accident.

## The full ecosystem

The CLI is the core, but there's more:

ESLint plugin — catches creep as you write code, not after:

  npm install -D eslint-plugin-client-creep
Enter fullscreen mode Exit fullscreen mode
  // eslint.config.js
  import clientCreep from 'eslint-plugin-client-creep'
  export default [clientCreep.configs.recommended]
Enter fullscreen mode Exit fullscreen mode

GitHub Action — posts a PR comment showing new creep introduced by a branch:

  - uses: DhruvilChauahan0210/client-creep@main
    with:
      ci: true
      budget: 500  # fail if > 500 KB client JS
Enter fullscreen mode Exit fullscreen mode

Dashboard — track creep trend over time across PRs:
client-creep-dashboard.vercel.app

VS Code extension — inline diagnostics as you type (Marketplace publish coming soon).

## Interactive HTML graph

  npx client-creep --html
Enter fullscreen mode Exit fullscreen mode

Generates a D3 force-directed graph of your entire import graph, color-coded by component type. Click any
node to see its import chain and why it's client. Useful for showing the team where the real problems are.

## CI usage

  # Fail if any accidental creep exists
  npx client-creep --ci

  # Fail if client JS exceeds 500 KB
  npx client-creep --budget 500
Enter fullscreen mode Exit fullscreen mode

## How it works

It's a static analyzer — no need to run your app.

  1. Globs all .ts/.tsx files, skipping node_modules and .next
  2. Parses each file with a Babel AST, detects "use client" and extracts imports
  3. Resolves imports including tsconfig path aliases and monorepo workspace packages
  4. Builds a directed import graph and propagates client boundaries via BFS
  5. Flags nodes with no detected client signals (hooks, browser globals, event handlers) as accidental creep

Works in monorepos too — auto-detects pnpm/turbo/yarn workspaces and resolves cross-package imports.


GitHub: github.com/DhruvilChauahan0210/client-creep
npm: npx client-creep

Would love feedback — especially if you run it on your codebase and find something unexpected.

Top comments (0)