How Turborepo Builds Its Graph
Turborepo creates its dependency graph by analyzing the package.json files within your monorepo. It looks for two key things:
-
dependencies: What external npm packages and, crucially, what other local workspaces a package depends on. -
devDependencies: Similar todependencies, these also define relationships between workspaces.
When you run turbo run build --graph, Turborepo:
- Discovers all workspaces.
- For each workspace, it reads its
package.json. - It draws a dependency edge from a package (e.g.,
web-app) to any other workspace listed in itsdependenciesordevDependencies(e.g.,ui-components,utils). - It uses this graph to determine the correct order of execution and what can be cached.
Why Lazy (Dynamic) Imports Are Ignored
Lazy loading, typically done with syntax like import('./some-module') or await require('./other-module'), is a runtime concept.
- Turborepo operates at the build/package level. It runs tasks (like
build,test,lint) on entire packages. It doesn't analyze the source code inside those packages to see how modules are imported at runtime. - Dynamic imports are resolved by the application bundler (e.g., Webpack, Vite, Rollup) during the application's build process, not by Turborepo's task runner.
- The dependency graph Turborepo builds is a task-level graph, not a source-code-level graph.
A Practical Example
Imagine this monorepo structure:
my-turborepo/
├── apps/
│ └── next-app/
│ ├── package.json
│ └── pages/
│ └── index.js
└── packages/
├── ui/
│ ├── Button.jsx
│ └── package.json
└── utils/
├── helpers.js
└── package.json
apps/next-app/package.json:
{
"name": "next-app",
"dependencies": {
"ui": "workspace:*",
"utils": "workspace:*"
}
}
apps/next-app/pages/index.js:
// Static import - Turborepo knows about this via the package.json
import { Button } from 'ui';
// Dynamic import - Invisible to Turborepo's graph
const DynamicComponent = dynamic(() => import('../components/HeavyComponent'));
function HomePage() {
return (
<div>
<Button>Click Me</Button>
<DynamicComponent />
</div>
);
}
What Turborepo Sees:
The graph will show next-app -> depends on -> ui and utils. It will ensure the build tasks for ui and utils run before the build task for next-app.
What Turborepo Does NOT See:
- The dynamic import of
HeavyComponent. - Any other dynamic import within the source code.
Implications and Best Practices
Correct Caching: This behavior is correct and desired. If you change a dynamically imported module inside the
next-appitself (likeHeavyComponent.js), you don't want Turborepo to invalidate the cache for the entireuiorutilspackages. You only want to rebuildnext-app. Your application bundler (Next.js, Vite, etc.) handles the granular caching of those internal source files.Defining Dependencies Correctly: The takeaway is that you must declare all package-level dependencies in your
package.jsonfiles. Ifnext-appuses a component from theuipackage, even if it's dynamically imported, theuipackage must be listed as a dependency. Turborepo relies solely on this declaration.
In summary: Turborepo's graph is built from package.json declarations, not from scanning source code for import statements. Lazy loading is a runtime concern handled by your bundler and is invisible to Turborepo's task scheduling and caching mechanism.
Top comments (0)