Turbopack
Turbopack is an incremental bundler optimized for JavaScript and TypeScript, written in Rust.
This document aims to cover documentation for developing/debugging Turbopack. For information on using Turbopack for Next.js applications, see the Next.js documentation.
What to read
If this is your first time setting up a development environment, recommend that you read Setup dev environment and How to run tests first.
The implementation of Next.js features in Turbopack is covered in the Next.js integration
section, and How Turbopack works
covers high level documentation for how turbopack's internal works.
Lastly, API references contains all of Turbopack-related package's api documentation based on rustdoc.
Setting up a development environment
What to install
You should have the following tools installed on your system.
- rust
- node.js - We recommend a version manager like fnm
- pnpm - We recommend using Corepack
- Your favorite development editor - Recommend editors that can use Rust-analyzer
- Other toolchains such as xcode or gcc may be required depending on the system environment and can be discussed in #team-turbopack.
Nextpack
Turbopack currently utilizes code from two repositories, Next.js and Turbo, so it can sometimes be cumbersome to incorporate changes from either repository. Nextpack allows you to work on the two repositories for development as linked, single workspace. We recommend using Nextpack unless you have a specific reason not to.
You can read about how you can set up a development environment in the Nextpack repository.
Upgrading Turbopack in Next.js
For Next.js to reflect the changes you made on the Turbo side, you need to release a new version. Turbopack is not currently releasing packages to crates.io, releasing new versions based on git tags instead. By default, we release a nightly version every day, but if you need a new change you can create a tag manually. The format of the tag is turbopack-YYMMDD.n
. When creating PR for Next.js, you should use Nextpack's next-link
script to properly include the runtime code used by Turbopack.
How to run tests
Next.js
Most Turbopack testing is done through integration tests in Next.js. Currently (as of February 2024), Turbopack does not pass all of the Next.js test cases. When running tests, some tests are not performed because of the distinction between Turbopack and traditional tests.
Turbopack test manifestcontains a list of tests that either pass or fail, and test runner selectively runs passing test on the CI.
In general, you can use CI to run full test cases and check for regressions, and run individual tests on your local machine in the following ways.
// Run single test file
TURBOPACK=1 pnpm test-dev test/e2e/edge-can-use-wasm-files/index.test.ts
// Run specific test case in the specified file
TURBOPACK=1 pnpm test-dev test/e2e/edge-can-use-wasm-files/index.test.ts -t "middleware can use wasm files lists the necessary wasm bindings in the manifest"
Few environment variable can control test's behavior.
// Run Next.js in Turbopack mode. Required when running Turbopack tests.
TURBOPACK=1
// Specifies the location of the next-swc napi binding binary.
// Allows to use locally changed turbopack binaries in certain app (i.e vercel/front) without replacing
// whole next.js installation
__INTERNAL_CUSTOM_TURBOPACK_BINDINGS=${absolute_path_to_node_bindings}
Updating test snapshot
Most of test cases have snapshot will have branching like below:
if (process.env.TURBOPACK) {
expect(some).toMatchInlineSnapshot(..)
} else {
expect(some).toMatchInlineSnapshot(..)
}
This allows you to test different output snapshots per bundler without changing snapshots. When there are changes in those snapshots, test should ran against webpack & Turbopack both to ensure both snapshots are reflecting changes.
Check the status of tests
There are few places we can track the status of tests.
Turbopack
Turbo have some tests for the Turbopack as well. It is recommended to use nextest to run the tests.
Test flakiness
Next.js and Turbopack's test both can be flaky. In most cases, retry CI couple of times would resolve the issue. If not and unsure, check in #team-turbopack
or #coord-next-turbopack
Tracing Turbopack
Turbopack comes with a tracing feature that allows to keep track of executions and their runtime and memory consumption. This is useful to debug and optimize the performance of your application.
Logging
Inside of Next.js one can enable tracing with the NEXT_TURBOPACK_TRACING
environment variable.
It supports the following special preset values:
1
oroverview
: Basic user level tracing is enabled. (This is the only preset available in a published Next.js release)next
: Same asoverview
, but with lower-leveldebug
andtrace
logs for Next.js's own cratesturbopack
: Same asnext
, but with lower-leveldebug
andtrace
logs for Turbopack's own cratesturbo-tasks
: Same asturbopack
, but also with verbose tracing of every Turbo-Engine function execution.
Alternatively, any syntax supported by tracing_subscriber::filter::EnvFilter
can be used.
For the more detailed tracing a custom Next.js build is required. See Setup for more information how to create one.
With this environment variable, Next.js will write a .next/trace.log
file with the tracing information in a binary format.
Viewer
To visualize the content of .next/trace.log
, the turbo-trace-viewer can be used.
This tool connects the a WebSocket on port 57475 on localhost to connect the the trace-server.
One can start the trace-server with the following command:
cargo run --bin turbo-trace-server --release -- /path/to/your/trace.log
The trace viewer allows to switch between multiple different visualization modes:
- Aggregated spans: Spans with the same name in the same parent are grouped together.
- Individual spans: Every span is shown individually.
- ... in order: Spans are shown in the order they occur.
- ... by value: Spans are sorted and spans with the largest value are shown first.
- Bottom-up view: Instead of showing the total value, the self value is shown.
And there different value modes:
- Duration: The CPU time of each span is shown.
- Allocated Memory: How much memory was allocated during the span.
- Allocations: How many allocations were made during the span.
- Deallocated Memory: How much memory was deallocated during the span.
- Persistently allocated Memory: How much memory was allocated but not deallocated during the span. It survives the span.
Turbo Tasks: Concepts
Introduction
The Turbo Tasks library provides is an incremental computation system that uses macros and types to automate the caching process. Externally, it is known as the Turbo engine.
It draws inspiration from webpack, Salsa (which powers Rust-Analyzer and Ruff), Parcel, the Rust compiler’s query system, Adapton, and many others.
Functions and Tasks
#[turbo_tasks::function]
s are memoized functions within the build process. An instance of a function with arguments is called a task. They are responsible for:
- Tracking Dependencies: Each task keeps track of its dependencies to determine when a recomputation is necessary. Dependencies are tracked when a
Vc<T>
(Value Cell) is awaited. - Recomputing Changes: When a dependency changes, the affected tasks are automatically recomputed.
- Parallel Execution: Every task is spawned as a Tokio task, which uses Tokio's multithreaded work-stealing executor.
Task Graph
All tasks and their dependencies form a task graph.
This graph is crucial for invalidation propagation. When a task is invalidated, the changes propagate through the graph, triggering rebuilds where necessary.
Incremental Builds
The Turbo engine employs a bottom-up approach for incremental builds.
By rebuilding invalidated tasks, only the parts of the graph affected by changes are rebuilt, leaving untouched parts intact. No work is done for unchanged parts.
Turbo Tasks: Tasks and Functions
A Task is an instance of a function along with its arguments. These tasks are the fundamental units of work within the build system.
Defining Tasks
Tasks are defined using Rust functions annotated with the #[turbo_tasks::function]
macro.
#![allow(unused)] fn main() { #[turbo_tasks::function] fn add(a: i32, b: i32) -> Vc<Something> { // Task implementation goes here... } }
- Tasks can be implemented as either a synchronous or asynchronous function.
- Arguments must implement the
TaskInput
trait. Usually these are primitives or types wrapped inVc<T>
. For details, see the task inputs documentation. - The external signature of a task always returns
Vc<T>
. This can be thought of as a lazily-executed promise to a value of typeT
. - Generics (type or lifetime parameters) are not supported in task functions.
External Signature Rewriting
The #[turbo_tasks::function]
macro rewrites the arguments and return values of functions. The rewritten function signature is referred to as the "external signature".
Argument Rewrite Rule
-
Function arguments with the
ResolvedVc<T>
type are rewritten toVc<T>
.- The value cell is automatically resolved when the function is called. This reduces the work needed to convert between
Vc<T>
andResolvedVc<T>
types. - This rewrite applies for
ResolvedVc<T>
types nested inside ofOption<ResolvedVc<T>>
andVec<ResolvedVc<T>>
. For more details, refer to theFromTaskInput
trait.
- The value cell is automatically resolved when the function is called. This reduces the work needed to convert between
-
Method arguments of
&self
are rewritten toself: Vc<Self>
.
Return Type Rewrite Rules
- A return type of
Result<Vc<T>>
is rewritten intoVc<T>
.- The
Result<Vc<T>>
return type allows for idiomatic use of the?
operator inside of task functions.
- The
- A function with no return type is rewritten to return
Vc<()>
instead of()
. - The
impl Future<Output = Vc<T>>
type implicitly returned by an async function is flattened into theVc<T>
type, which represents a lazy and re-executable version of theFuture
.
Some of this logic is represented by the TaskOutput
trait and its associated Return
type.
External Signature Example
As an example, the method
#![allow(unused)] fn main() { #[turbo_tasks::function] async fn foo( &self, a: i32, b: Vc<i32>, c: ResolvedVc<i32>, d: Option<Vec<ResolvedVc<i32>>>, ) -> Result<Vc<i32>> { // ... } }
will have an external signature of
#![allow(unused)] fn main() { fn foo( self: Vc<Self>, // was: &self a: i32, b: Vc<i32>, c: Vc<i32>, // was: ResolvedVc<i32> d: Option<Vec<Vc<i32>>>, // was: Option<Vec<ResolvedVc<i32>>> ) -> Vc<i32>; // was: impl Future<Output = Result<Vc<i32>>> }
Methods with a Self Argument
Tasks can be methods associated with a value or a trait implementation using the arbitrary_self_types
nightly compiler feature.
Inherent Implementations
#![allow(unused)] fn main() { #[turbo_tasks::value_impl] impl Something { #[turbo_tasks::function] fn method(self: Vc<Self>, a: i32) -> Vc<SomethingElse> { // Receives the full `Vc<Self>` type, which we must `.await` to get a // `ReadRef<Self>`. vdbg!(self.await?.some_field); // The `Vc` type is useful for calling other methods declared on // `Vc<Self>`, e.g.: self.method_resolved(a) } #[turbo_tasks::function] fn method_resolved(self: ResolvedVc<Self>, a: i32) -> Vc<SomethingElse> { // Same as above, but receives a `ResolvedVc`, which can be `.await`ed // to a `ReadRef` or dereferenced (implicitly or with `*`) to `Vc`. vdbg!(self.await?.some_field); // The `ResolvedVc<Self>` type can be used to call other methods // declared on `Vc<Self>`, e.g.: self.method_ref(a) } #[turbo_tasks::function] fn method_ref(&self, a: i32) -> Vc<SomethingElse> { // As a convenience, receives the fully resolved version of `self`. This // does not require `.await`ing to read. // // It can access fields on the struct/enum and call methods declared on // `Self`, but it cannot call other methods declared on `Vc<Self>` // (without cloning the value and re-wrapping it in a `Vc`). Vc::cell(SomethingElse::new(self.some_field, a)) } } }
-
Declaration Location: The methods are defined on
Vc<T>
(i.e.Vc<Something>::method
andVc<Something>::method2
), not on the inner type. -
&self
Syntactic Sugar: The&self
argument of a#[turbo_tasks::function]
implicitly reads the value fromself: Vc<Self>
. -
External Signature Rewriting: All of the signature rewrite rules apply here.
self
can beResolvedVc<T>
.async
andResult<Vc<T>>
return types are supported.
Trait Implementations
#![allow(unused)] fn main() { #[turbo_tasks::value_impl] impl Trait for Something { #[turbo_tasks::function] fn method(self: Vc<Self>, a: i32) -> Vc<SomethingElse> { // Trait method implementation... // // `self: ResolvedVc<Self>` and `&self` are also valid argument types! } } }
For traits, only the external signature (after rewriting) must align with the trait definition.
Turbo Tasks: Value Cells
Value Cells (the Vc<T>
type) represent the pending result of a computation, similar to a cell in a spreadsheet. When a Vc
's contents change, the change is propagated by invalidating dependent tasks.
The contents (T
) of a Vc<T>
are always a VcValueType
. These types are created using the #[turbo_tasks::value]
macro.
Understanding Cells
A Cell is a storage location for data associated with a task. Cells provide:
- Immutability: Once a value is stored in a cell, it becomes immutable until that task is re-executed.
- Recomputability: If invalidated or cache evicted, a cell's contents can be re-computed by re-executing its associated task.
- Dependency Tracking: When a cell's contents are read (with
.await
), the reading task is marked as dependent on the cell.
Cells are stored in arrays associated with the task that constructed them. A Vc
can either point to a specific cell (this is a "resolved" cell), or the return value of a function (this is an "unresolved" cell).
Constructing a Cell
Most types using the #[turbo_tasks::value]
macro are given a .cell()
method. This method returns a Vc
of the type.
Transparent wrapper types that use #[turbo_tasks::value(transparent)]
cannot define methods on their wrapped type, so instead the Vc::cell
function can be used to construct these types.
Updating a Cell
Every time a task runs, its cells are re-constructed.
When .cell()
or Vc::cell
is called, the value is compared to the previous execution's using PartialEq
. If it differs, the cell is updated, and all dependent tasks are invalidated. This behavior can be overridden to always invalidate using the cell = "new"
argument.
Because cells are keyed by a combination of their type and construction order, task functions should have a deterministic execution order. A function with inconsistent ordering may result in wasted work by invalidating additional cells, though it will still give correct results.
You should use types with deterministic behavior such as IndexMap
or BTreeMap
in place of types like HashMap
(which gives randomized iteration order).
Reading from Cells
To read the value from a cell, Vc<T>
implements IntoFuture
:
#![allow(unused)] fn main() { let vc: Vc<T> = ...; let value: ReadRef<T> = vc.await?; }
A ReadRef<T>
represents a reference-counted snapshot of a cell's value at a given point in time.
Vc
s must be awaited because we may need to wait for the associated task to finish executing. This operation may fail.
Reading from a Vc
registers the current task as a dependent of the cell, meaning the task will be re-computed if the cell's value changes.
Eventual Consistency
Because turbo_tasks
is [eventually consistent], two adjacent .await
s of the same Vc<T>
may return different values. If this happens, the task will eventually be invalidated and re-executed by a strongly consistent root task.
Tasks affected by a read inconsistency can return errors. These errors will be discarded by the strongly consistent root task. Tasks should never panic.
Currently, all inconsistent tasks are polled to completion. Future versions of the turbo_tasks
library may drop tasks that have been identified as inconsistent after some time. As non-root tasks should not perform side-effects, this should be safe, though it may introduce some issues with cross-process resource management.
Defining Values
Values intended for storage in cells must be defined with the #[turbo_tasks::value]
attribute:
- Comparison: By default, values implement
PartialEq + Eq
for comparison.- Opt-out with
#[turbo_tasks::value(eq = "none")]
. - For custom comparison logic, use
#[turbo_tasks::value(eq = "manual")]
.
- Opt-out with
- Serialization: Values also implement
Serialize + Deserialize
for persistent caching.- Opt-out with
#[turbo_tasks::value(serialization = "none")]
.
- Opt-out with
- Tracing: Values implement
TraceRawVcs
to facilitate the discovery process of nestedVc
s.- This may be used for tracing garbage collection in the future.
- Debug Printing: Values implement
ValueDebugFormat
for debug printing, resolving nestedVc
s to their values.- Debug-print
Vc
types with thevdbg!(...)
macro.
- Debug-print
To opt-out of certain requirements for fields within a struct, use the following attributes:
#[turbo_tasks(debug_ignore)]
: Skips the field for debug printing.#[turbo_tasks(trace_ignore)]
: Skips the field for tracing nestedVc
s. It must not have nestedVc
s to be correct. This is useful for fields containing foreign types that can not implementVcValueType
.
Code Example
Here's an example of defining a value for use in a cell:
#![allow(unused)] fn main() { #[turbo_tasks::value] struct MyValue { // Fields go here... } }
In this example, MyValue
is a struct that can be stored in a cell.
Enforcing Cell Resolution
As mentioned earlier, a Vc
may either be "resolved" or "unresolved":
- Resolved: Points to a specific cell constructed within a task.
- Unresolved: Points to the return value of a task (a function with a specific set of arguments). This is a lazily evaluated pointer to a resolved cell within the task.
Internally, a Vc
may have either representation, but sometimes you want to enforce that a Vc
is either resolved:
-
Equality: When performing equality comparisons between
Vc
s, it's important to ensure that both sides are resolved, so that you are comparing equivalent representations ofVc
s. All task arguments are automatically resolved before function execution so that task memoization can compare arguments by cell id. -
Resolved
VcValueType
s: In the future, all fields inVcValueType
s will be required to useResolvedVc
instead ofVc
. This will facilitate more accurate equality comparisons.
The ResolvedVc
Type
ResolvedVc<T>
is a subtype of Vc<T>
that enforces resolution statically with types. It implements Deref<Target = Vc<T>>
and behaves similarly to Vc
types.
Constructing a ResolvedVc
ResolvedVc
s can be constructed using generated .resolved_cell()
methods or with the ResolvedVc::cell()
function (for transparent wrapper types).
A Vc
can be implicitly converted to a ResolvedVc
by using a ResolvedVc
type in a #[turbo_tasks::function]
argument via the external signature rewriting rules.
Vc
s can be be explicitly converted to ResolvedVc
s using .to_resolved().await?
. This may require waiting on the task to finish executing.
Reading a ResolvedVc
Even though a Vc
may be resolved as a ResolvedVc
, we must still use .await?
to read it's value, as the value could be invalidated or cache-evicted.
Calls
Introduction
In Turbo-Engine, calls to turbo task functions are central to the operation of the system. This section explains how calls work within the Turbo-Engine framework, including the concepts of unresolved and resolved Vc<T>
.
Making Calls
#![allow(unused)] fn main() { let vc: Vc<T> = some_turbo_tasks_function(); }
When you invoke a Turbo-Tasks Function, it returns a Vc<T>
, which is a pointer to the task's return value. Here's what happens during a call:
- Immediate Return: The call returns instantly, and the task is queued for computation.
- Unresolved Vc: The returned
Vc
is a so called "unresolved"Vc
, meaning it points to a return value that may not yet be computed.
Resolved vs Unresolved Vc
s
- Unresolved Vc: An "unresolved"
Vc
points to a return value of a Task - Resolved Vc: A "resolved"
Vc
points to a cell computed by a Task.
Resolving Vc
s
To compare Vc
values or ensure they are ready for use, you can resolve them:
#![allow(unused)] fn main() { let resolved = vc.resolve().await?; }
- Reading: It's not mandatory to resolve
Vc
s before reading them. ReadingVc
s will first resolve it and then read the cell value. - Dependency Registration: Resolving a
Vc
registers the current task as a dependent of the task output, ensuring that the task is recomputed if the return value changes. - Usage as Key: Resolving
Vc
s is mandatory when using them as keys ofHashMap
orHashSet
for the cell identity.
Importance of Resolution
Resolution is crucial for the memoization process:
- Memoization:
Vc
s used as arguments are identified by their pointer. - Automatic Resolution: Unresolved
Vc
s passed to turbo task functions are resolved automatically, ensuring that functions receive resolvedVc
s for memoization.- Intermediate Task: An intermediate task is created internally to handle the resolution of arguments.
Performance Considerations
While automatic resolution is convenient, it introduces the slight overhead of the intermediate Task:
- Manual Resolution: In performance-critical scenarios, resolving
Vc
s manually may be beneficial to reduce overhead.
Code Example
Here's an example of calling a turbo task function and resolving its Vc
:
#![allow(unused)] fn main() { // Calling a turbo task function let vc: Vc<MyType> = compute_something(); // Resolving the Vc for direct comparison or use let resolved_vc: MyType = vc.resolve().await?; }
Task inputs
Introduction
In Turbo-Engine, task functions are the cornerstone of the build system. To ensure efficient and correct builds, only specific types of arguments, known as TaskInput
s, can be passed to these functions. This guide details the requirements and types of task inputs permissible in Turbo-Engine.
Requirements for Task Inputs
Serialization
For persistent caching to function effectively, arguments must be serializable.
This ensures that the state of a task can be saved and restored without loss of fidelity.
This means the implementation of Serialize
and Deserialize
traits is necessary for task inputs.
HashMap Key
Arguments are also used as keys in a HashMap, necessitating the implementation of PartialEq
, Eq
, and Hash
traits.
Types of Task Inputs
Vc<T>
A Vc<T>
is a pointer to a cell and serves as a "pass by reference" argument:
- Memoization: It's important to note that
Vc<T>
is keyed by pointer for memoization purposes. Identical values in different cells are treated as distinct. - Singleton Pattern: To ensure memoization efficiency, the Singleton pattern can be employed to guarantee that identical values yield the same
Vc
. For more info see Singleton Pattern Guide.
Deriving TaskInput
Structs or enums can be made into task inputs by deriving TaskInput
:
#![allow(unused)] fn main() { #[derive(TaskInput)] struct MyStruct { // Fields go here... } }
- Pass by Value: Derived
TaskInput
types are passed by value, which involves cloning the value multiple times. It's recommended to ensure that these types are inexpensive to clone.
Deprecated: Value<T>
The Value<T>
type is a legacy way to define task inputs and should no longer be used.
Future Consideration: Arc<T>
The use of Arc<T>
where T
implements TaskInput
is under consideration for future implementation.
Conclusion
Understanding and utilizing the correct task inputs is vital for the smooth operation of Turbo-Engine. By adhering to the guidelines outlined in this document, developers can ensure that their tasks are set up for success, leading to faster and more accurate builds.
Traits
#[turbo_tasks::value_trait]
pub trait MyTrait {
fn method(self: Vc<Self>, a: i32) -> Vc<Something>;
// External signature: fn method(self: Vc<Self>, a: i32) -> Vc<Something>
async fn method2(&self, a: i32) -> Result<Vc<Something>> {
// Default implementation
}
}
#[turbo_tasks::value_impl]
pub trait OtherTrait: MyTrait + ValueToString {
// ...
}
#[turbo_tasks::value_trait]
annotation is required to define traits in Turbo-Engine.- All methods are Turbo-Engine functions.
self
argument is alwaysVc<Self>
.- Default implementation are supported and behave similar to defining Turbo-Engine functions.
Vc<Box<dyn MyTrait>>
is a Vc to a trait.
Implementing traits
#[turbo_tasks::value_impl]
impl MyTrait for Something {
fn method(self: Vc<Self>, a: i32) -> Vc<Something> {
// Implementation
}
fn method2(&self, a: i32) -> Vc<Something> {
// Implementation
}
}
Upcasting
Vcs can be upcasted to a trait Vc:
let something_vc: Vc<Something> = ...;
let trait_vc: Vc<Box<dyn MyTrait>> = Vc::upcast(something_vc);
Downcasting
Vcs can also be downcasted to a concrete type of a subtrait:
let trait_vc: Vc<Box<dyn MyTrait>> = ...;
if let Some(something_vc) = Vc::try_downcast_type::<Something>(trait_vc).await? {
// ...
}
let trait_vc: Vc<Box<dyn MyTrait>> = ...;
if let Some(something_vc) = Vc::try_downcast::<Box<dyn OtherTrait>>(trait_vc).await? {
// ...
}
There is a compile-time check that the source trait is implemented by the target trait/type.
Note: This will resolve the Vc and have similar implications as .resolve().await?
.
Sidecasting
let trait_vc: Vc<Box<dyn MyTrait>> = ...;
if let Some(something_vc) = Vc::try_sidecast::<Box<dyn UnrelatedTrait>>(trait_vc).await? {
// ...
}
This won't do any compile-time checks, so downcasting should be preferred if possible.
Note: This will resolve the Vc and have similar implications as .resolve().await?
.
Patterns
Singleton
The Singleton pattern is a design pattern that restricts the instantiation of a class to a single instance. This is useful for a system that requires a single, global point of access to a particular resource.
In the context of Turbo-Engine, the Singleton pattern can be used to intern a value into a Vc
. This ensures that for a single value, there is exactly one resolved Vc
. This allows Vcs
to be compared for equality and used as keys in hashmaps or hashsets.
Usage
To use the Singleton pattern in Turbo-Engine:
- Make the
.cell()
method private (Use#[turbo_tasks::value]
instead of#[turbo_tasks::value(shared)]
). - Only call the
.cell()
method in a Turbo-Engine function which acts as a constructor. - The constructor arguments act as a key for the constructor task which ensures that the same value is always celled in the same task.
- Keep in mind that only resolved
Vcs
are equal. UnresolvedVcs
might not be equal to each other.
Example
Here is an example of how to implement the Singleton pattern in Turbo-Engine:
#[turbo_tasks::value]
struct SingletonString {
value: String,
}
#[turbo_tasks::value_impl]
impl SingletonString {
#[turbo_tasks::function]
fn new(value: String) -> Vc<SingletonString> {
Self { value }.cell()
}
}
#[test]
fn test_singleton() {
let a1 = SingletonString::new("a".to_string()).resolve().await?;
let a2 = SingletonString::new("a".to_string()).resolve().await?;
let b = SingletonString::new("b".to_string()).resolve().await?;
assert_eq!(a1, a2); // Resolved Vcs are equal
assert_neq!(a1, b);
let set = HashSet::from([a1, a2, b]);
assert_eq!(set.len(), 2); // Only two different values
}
In this example, SingletonString
is a struct that contains a single String
value. The new
function acts as a constructor for SingletonString
, ensuring that the same string value is always celled in the same task.
Filesystem
For correct cache invalidation it's important to not read from external things in a Turbo-Engine function, without taking care of invalidations of that external thing.
To allow using filesystem operations correctly, Turbo-Engines provides a filesystem API which includes file watching for invalidation.
It's important to only use the filesystem API to access the filesystem.
API
Constructing
// Create a filesystem
let fs = DiskFileSystem::new(...);
// Get the root path, returns a Vc<FileSystemPath>
let fs_path = fs.root();
Navigating
// Access a sub path (error when leaving the root)
let fs_path = fs_path.join("sub/directory/file.txt".to_string());
// Append to the filename (file.txt.bak)
fs_path.append(".bak".to_string());
// Append to the basename (file.copy.txt)
fs_path.append_to_stem(".copy".to_string());
// Change the extension (file.raw)
fs_path.with_extension("raw".to_string());
// Access a sub path (returns None when leaving the root)
fs_path.try_join("../../file.txt".to_string());
// Access a sub path (returns None when leaving the current path)
fs_path.try_join_inside("file.txt".to_string());
// Get the parent directory as Vc<FileSystemPath>
fs_path.parent();
// Get all glob matches
fs_path.read_glob(...);
Metadata access
// Gets the filesystem
fs_path.fs();
// Gets the extension
fs_path.extension();
// Gets the basename
fs_path.file_stem();
Comparing
// Is the path inside of the other path
fs_path.is_inside(other_fs_path);
// Is the path inside or equal to the other path
fs_path.is_inside_or_equal(other_fs_path);
Reading
// Returns the type of the path (file, directory, symlink, ...)
// This is based on read_dir of the parent directory and much more efficient than .metadata()
fs_path.get_type();
// Reads the file content
fs_path.read();
// Reads a symlink content
fs_path.read_link()
// Reads the file content as JSON
fs_path.read_json();
// Read a directory. Returns a map of new Vc<FileSystemPath>s
fs_path.read_dir();
// Gets a Vc<Completion> that changes when the file content changes
fs_path.track();
// Reads metadata of a file
fs_path.metadata();
// Resolves all symlinks in the path and returns the real path
fs_path.realpath();
// Resolves all symlinks in the path and returns the real path and a list of symlinks
fs_path.real_path_with_symlinks();
Writing
// Write the file content
fs_path.write(...);
// Write the file content
fs_path.write_link(...);
Advanced
Registry
Registration of value types, tait types and functions is required for serialization (Persistent Caching).
This is handled by the turbo-tasks-build
create.
Required setup in every crate using Turbo-Engine:
// build.rs
use turbo_tasks_build::generate_register;
fn main() {
generate_register();
}
// src/lib.rs
// ...
pub fn register() {
// Call register for all dependencies
turbo_tasks::register();
// Run the generated registration code for items of the own crate
include!(concat!(env!("OUT_DIR"), "/register.rs"));
}
In test cases using Turbo Engine:
include!(concat!(env!("OUT_DIR"), "/register_test_<test-case>.rs"));
Why?
During deserialization Turbo Engine need to look up the deserialization code needed from the registry since type information is not statically known.
State
Turbo-Engine requires all function to be pure and deterministic, but sometimes one need to manage some state during the execution. State is ok as long as functions that read the state are correctly invalidated when the state changes.
To help with that Turbo Engine comes with a State<T>
struct that automatically handles invalidation.
Usage
#[turbo_tasks::value]
struct SomeValue {
immutable_value: i32,
mutable_value: State<i32>,
}
#[turbo_tasks::value_impl]
impl SomeValue {
#[turbo_tasks::function]
fn new(immutable_value: i32, mutable_value: i32) -> Vc<SomeValue> {
Self {
immutable_value,
mutable_value: State::new(mutable_value),
}
.cell()
}
#[turbo_tasks::function]
fn get_mutable_value(&self) -> Vc<i32> {
// Reading the state will make the current task depend on the state
// Changing the state will invalidate `get_mutable_value`.
let value = self.mutable_value.get();
Vc::cell(value)
}
#[turbo_tasks::function]
fn method(&self) {
// Writing the state will invalidate all reader of the state
// But only if the state has been changed.
self.mutable_value.update_conditionally(|value: &mut i32| {
*old += 1;
// Return true when the value has been changed
true
});
// There are more ways to update the state:
// Sets a new value. Compared this value with the old value and only update if the value has changed.
// Requires `PartialEq` on the value type.
self.mutable_value.set(42);
// Sets a new value unconditionally. Always update the state and invalidate all readers.
// Doesn't require `PartialEq` on the value type.
self.mutable_value.set_unconditionally(42);
}
}
State and Persistent Caching
TODO (Not implemented yet)
State is persistent in the Persistent Cache.
Best Practice
Use State
only when necessary. Prefer pure functions and immutable values.
Garbage collection
Determinism
All Turbo-Engine functions must be pure and deterministic. This means one should avoid using global state, static mutable values, randomness, or any other non-deterministic operations.
A notable mention is HashMap
and HashSet
. The order of iteration is not guaranteed and can change between runs. This can lead to non-deterministic builds.
Prefer IndexMap
and IndexSet
(or BTreeMap
and BTreeSet
) which have a deterministic order.
Architecture
This is a high level overview of Turbopack's architecture.
Core Concepts
Turbopack has these guiding principles:
Incremental
- Avoid operations that require broad/complete graph traversal.
- Changes should only have local effects.
- Avoid depending on "global" information (information from the whole app).
- Avoid "one to many" dependencies (depending on one piece of information from many sources).
Deterministic
- Everything need to be deterministic.
- Avoid randomness, avoid depending on timing.
- Be careful with non-deterministic datastructures (like
HashMap
,HashSet
).
Lazy
- Avoid computing more information than needed for a given operation.
- Put extra indirection when needed.
- Use decorators to transform data on demand.
Extensible
- Use traits.
- Avoid depending on concrete implementations (avoid casting).
- Make behavior configurable by passing options.
Simple to use
- Use good defaults. Normal use case should work out of the box.
- Use presets for common use cases.
Layers
Turbopack models the user's code as it travels through Turbopack in multiple ways, each of which can be thought of as a layer in the system below:
┌───────────────┬──────────────────────────────┐
│ Sources │ │
├───────────────┘ │
│ What "the user wrote" as │
│ application code │
└──────────────────────────────────────────────┘
┌───────────────┬──────────────────────────────┐
│ Modules │ │
├───────────────┘ │
│ The "compiler's understanding" of the │
│ application code │
└──────────────────────────────────────────────┘
┌───────────────┬──────────────────────────────┐
│ Output assets │ │
├───────────────┘ │
│ What the "target environment understands" as │
│ executable application │
└──────────────────────────────────────────────┘
Each layer is implemented as a decorator/wrapper around the previous layer in top-down order. In contrast to tools like webpack, where whole {source,module,chunk} graphs are built in serial, Turbopack builds lazily by successively wrapping an asset in each layer and building only what's needed without blocking on everything in a particular phase.
Sources
Sources are content of code or files before they are analyzed and converted into Modules. They might be the original source file the user has written, or a virtual source code that was generated. They might also be transformed from other Sources, e. g. when using a preprocessor like SASS or webpack loaders.
Sources do not model references (the relationships between files like through import
, sourceMappingURL
, etc.).
Each Source has an identifier composed of file path, query, fragment, and other modifiers.
Modules
Modules are the result of parsing, transforming and analyzing Sources. They include references to other modules as analyzed.
References can be followed to traverse a subgraph of the module graph. They implicitly from the module graph by exposing references.
Each Module has an identifier composed of file path, query, fragment, and other modifiers.
Output asset
Output assets are artifacts that are understood by the target environment. They include references to other output assets.
Output assets are usually the result of transforming one or more modules to a given output format. This can be a very simple transformation like just copying the Source content (like with static assets like images), or a complex transformation like chunking and bundling modules (like with JavaScript or CSS).
Output assets can be emitted to disk or served from the dev server.
Each Output asset has a file path.
Example
graph TD subgraph Source code src ---> b.src.js[ ] src ---> util util ---> d.src.css[ ] util ---> c.src.js[ ] end subgraph Module graph a.ts ---> b.js a.ts ---> c.js b.js ---> c.js c.js ---> d.css c.js ---> e.js[ ] b.js ---> f.js[ ] b.js -.-> b.src.js d.css -.-> d.src.css end subgraph Output graph main.js --> chunk.js main.js --> chunk2.js[ ] main.js -.-> a.ts main.js -.-> b.js main.js -.-> c.js end
Output format
An output format changes how a module is run in a specific target environment. The output format is chosen based on target environment and the input module.
graph LR Browser & Node.js ---> css_chunking[CSS chunking] Browser & Node.js ---> static_asset[Static asset] Browser & Node.js ---> js_chunking[JS chunking] Node.js ---> Rebasing vercel_nodejs[Vercel Node.js] ---> nft.json bundler[Bundler or module-native target] -.-> Modules
In the future, Turbopack should support producing modules as an output format, to be used by other bundlers or runtimes that natively support ESM modules.
Chunking
Chunking is the process that decides which modules are placed into which bundles, and the relationship between these bundles.
For this process a few intermediate concepts are used:
ChunkingContext
: A context trait which controls the process.ChunkItem
: A derived object from aModule
which combines the module with theChunkingContext
.ChunkableModule
: A trait which defines how a specificModule
can be converted into aChunkItem
.ChunkableModuleReference
: A trait which defines how a specificModuleReference
will interact with chunking.ChunkType
: A trait which defines how to create aChunk
fromChunkItem
s.Chunk
: A trait which represents a bundle ofChunkItem
s.
graph TB convert{{convert to chunk item}} ty{{get chunk type}} create{{create chunk}} EcmascriptModule -. has trait .-> Module CssModule -. has trait .-> Module Module -. has trait .-> ChunkableModule ChunkableModule === convert ChunkingContext --- convert convert ==> ChunkItem ChunkItem ==== ty EcmascriptChunkType -. has trait ..-> ChunkType CssChunkType -. has trait ..-> ChunkType ty ==> ChunkType ChunkType ==== create create ==> Chunk EcmascriptChunk -. has trait ..-> Chunk CssChunk -. has trait ..-> Chunk BrowserChunkingContext -. has trait .-> ChunkingContext NodeJsChunkingContext -. has trait .-> ChunkingContext
A Module
need to implement the ChunkableModule
trait to be considered for chunking. All references of these module that should be considered for chunking need to implement the ChunkableModuleReference
trait.
The chunking algorithm walks the module graph following these chunkable references to find all modules that should be bundled. These modules are converted into ChunkItem
s and a special algorithm decides which ChunkItem
s are placed into which Chunk
s.
One factor is for example the ChunkType
of each ChunkItem
since only ChunkItem
s with the same ChunkType
can be placed into the same Chunk
. Other factors are directory structure and chunk size based.
Once the ChunkItem
s that should be placed together are found, a Chunk
is created for each group of ChunkItem
s by using the method on ChunkType
.
An OutputAsset
is then created for each Chunk
which is eventually emitted to disk. The output format decides how a Chunk
is transformed into an OutputAsset
. These OutputAsset
s that are loaded together are called a chunk group.
The chunking also collects all modules into a AvailableModules
struct which is e. g. used for nested chunk groups to avoid duplicating modules that are already in the parent chunk group.
Mixed Graph
Not only can Turbopack travel between these different layers, but it can do so across and between different environments.
- It's possible to transitions between Output Formats
- For example, starting in ecmascript but referencing static assets with
new URL('./foo.png')
- For example, starting in ecmascript but referencing static assets with
- Embed/Reference Output Assets in Modules
- e.g. embedding base64 data url of a chunk inside another chunk
- SSR Page references client chunks for hydration
- embed urls to client-side chunks into server-generated html
- references output assets from a different chunking root from a different environment
- Client Component referenced from Server Component
- Client component wrapper (Server Component) references client chunks of a client component
All of this is made possible by having a single graph that models all source, modules, and output assets across all different environments.
Crates
Layers
We can split the crates into the following layers:
graph TB cli_utils[CLI Utils] Tools --> Facade CLI --> Facade & cli_utils Facade --> Output & Modules & Utils & Core Modules & Output --> Core & Utils Utils --> Core
Core
There crates are the core of turbopack, providing the basic traits and types. This e. g. handles issue reporting, resolving and environments.
turbopack-core
: Core types and traitsturbopack-resolve
: Specific resolving implementations
Utils
These crates provide utility functions and types that are used throughout the project.
turbopack-swc-utils
: SWC helpers that are shared between ecmascript and cssturbopack-node
: Node.js pool processing, webpack loaders, postcss transformturbopack-env
: .env file loading
Modules
These crates provide implementations of various module types.
turbopack-ecmascript
: The ecmascript module type (JavaScript, TypeScript, JSX), Tree Shakingturbopack-ecmascript-plugins
: Common ecmascript transforms (styled-components, styled-jsx, relay, emotion, etc.)turbopack-css
: The CSS module type (CSS, CSS Modules, SWC, lightningcss)turbopack-json
: The JSON module typeturbopack-static
: The static module type (images, fonts, etc.)turbopack-image
: Image processing (metadata, optimization, blur placeholder)turbopack-wasm
: The WebAssembly module typeturbopack-mdx
: The mdx module type
Output
These crates provide implementations of various output formats.
turbopack-browser
: Output format for the browser target, HMRturbopack-nodejs
: Output format for the Node.js targetturbopack-ecmascript-runtime
: The runtime code for bundlesturbopack-ecmascript-hmr-protocol
: The Rust side of the HMR protocol
Facade
These crates provide the facade to use Turbopack. They combine the different modules and output formats into presets and handle the default module rules.
turbopack-binding
: A crate reexporting all Turbopack crates for consumption in other projectsturbopack
: The main facade bundling all module types and rules when to apply them, presets for resolving and module processing
CLI
These crates provide the standalong CLI and the dev server.
turbopack-cli
: The Turbopack CLIturbopack-dev-server
: The on demand dev server implementation
CLI Utils
These crates provide utility functions for the CLI.
turbopack-cli-utils
: Helper functions for CLI toolingturbopack-trace-utils
: Helper function for tracing
Tools
These crates provide tools to test, benchmark and trace Turbopack.
turbopack-bench
: The Turbopack benchmarkturbopack-tests
: Integration tests for Turbopackturbopack-create-test-app
: A CLI to create a test application with configurable number of modulesturbopack-swc-ast-explorer
: A CLI tool to explore the SWC ASTturbopack-test-utils
: Helpers for testingturbopack-trace-server
: A CLI to open a trace file and serve it in the browser
@next/swc
The @next/swc
is a binding that makes features written in Rust available to the JavaScript runtime. This binding contains not only Turbopack, but also other features needed by Next.js (SWC, LightningCSS, etc.).
Package hierarchies
The figure below shows the dependencies between packages at a high level.
flowchart TD C(next-custom-transforms) --> A(napi) C(next-custom-transforms) --> B(wasm) D(next-core) --> A(napi) E(next-build) --> A(napi) F(next-api) --> A(napi) C(next-custom-transforms) --> D D(next-core) --> F(next-api) D(next-core) --> E(next-build)
next-custom-transforms
: provides next-swc specific SWC transform visitors. Turbopack, and the plain next-swc bindings (transform
) use these transforms. Since this is a bottom package can be imported in any place (turbopack / next-swc / wasm), it is important package do not contain specific dependencies. For example, using Turbopack's VC in this package will cause build failures to wasm bindings.next-core
: Implements Turbopack features for the next.js core functionality. This is also the place where Turbopack-specific transform providers (implementingCustomTransformer
) lives, which wraps swc's transformer in thenext-custom-transforms
.next-api
: Binding interface to the next.js provides a proper next.js functionality usingnext-core
.napi
/wasm
: The actual binding interfaces, napi for the node.js and wasm for the wasm. Note wasm bindings cannot import packages using turbopack's feature.
How to add new swc transforms
- Implement a new visitor in
next-custom-transforms
. It is highly encouraged to useVisitMut
instead ofFold
for the performance reasons. - Implement a new
CustomTransformer
underpackages/next-swc/crates/next-core/src/next_shared/transforms
to make a Turbopack ecma transform plugin, then adjust corresponding rules inpackages/next-swc/crates/next-core/src/(next_client|next_server)/context.rs
.
napi bindings feature matrix
Due to platform differences napi bindings selectively enables supported features. See below tables for the currently enabled features.
arch\platform | Linux(gnu) | Linux(musl) | Darwin | Win32 |
---|---|---|---|---|
ia32 | a,b,d,e | |||
x64 | a,b,d,e,f | a,b,d,e,f | a,b,d,e,f | a,b,d,e,f |
aarch64 | a,d,e,f | a,d,e,f | a,b,d,e,f | a,b,c,e |
- a:
turbo_tasks_malloc
, - b:
turbo_tasks_malloc_custom_allocator
, - c:
native-tls
, - d:
rustls-tls
, - e:
image-extended
(webp) - f:
plugin
Napi-rs
To generate bindings for the node.js, @next/swc
relies on napi-rs. Check napi
packages for the actual implementation.
Turbopack, turbopack-binding, and next-swc
Since Turbopack currently has features split across multiple packages and no official SDK, we've created a single entry point package, turbopack-binding, to use Turbopack features in next-swc. Turbopack-binding also reexports SWC, which helps to reduce the version difference between next-swc and turbopack. However, there are currently some places that use direct dependencies for macros and other issues.
Turbopack-binding is a package that is only responsible for simple reexports, allowing you to access each package by feature. The features currently in use in next-swc are roughly as follows.
SWC
__swc_core_binding_napi
: Features for using napi with swc__swc_core_serde
: Serde serializable ast__swc_core_binding_napi_plugin_*
: swc wasm plugin support__swc_transform_modularize_imports
: Modularize imports custom transform__swc_transform_relay
: Relay custom transform__swc_core_next_core
: Features required for next-core package
Turbo
__turbo
: Core feature to enable other turbo features.__turbo_tasks_*
: Features related to Turbo task.__turbo_tasks_malloc_*
: Custom memory allocator features.
Turbopack
__turbopack
: Core feature to enable other turbopack features.__turbopack_build
: Implements functionality for production builds__turbopack_cli_utils
: Formatting utilities for building CLIs around turbopack__turbopack_core
: Implements most of Turbopack's core structs and functionality. Used by many Turbopack crates__turbopack_dev
: Implements development-time builds__turbopack_ecmascript
: Ecmascript parse, transform, analysis__turbopack_ecmascript_plugin
: Entrypoint for the custom swc transforms__turbopack_ecmascript_hmr_protocol
__turbopack_ecmascript_runtime
: Runtime code implementing chunk loading, hmr, etc.__turbopack_env
: Support forprocess.env
anddotenv
__turbopack_static
: Support for static assets__turbopack_image_*
: Native image decoding / encoding support. Some codecs have separate features (avif, etcs)__turbopack_node
: Evaluates JavaScript code from Rust via a Node.js process pool__turbopack_trace_utils
Other features
__feature_auto_hash_map
__feature_node_file_trace
: Node file trace__feature_mdx_rs
: Mdx compilation support
Next.rs api
Contexts
TBD
Alias and resolve plugins
Turbopack supports aliasing imports similar to Webpack's 'alias' config. It is being applied when Turbopack runs its module resolvers.
Setting up alias
To set the alias, use the import_map
in each context's ResolveOptionContext
. ResolveOptionContext
also has fallback_import_map
which will be applied when a request to resolve cannot find any corresponding imports.
let resolve_option_context = ResolveOptionContext {
..
import_map: Some(next_client_import_map),
fallback_import_map: Some(next_client_fallback_import_map),
}
These internal mapping options will be applied at the end among any other resolve options. For example, if user have tsconfig
's compilerOptions.paths
alias, it'll take precedence.
ImportMap
can be either 1:1 match (AliasPattern::Exact
) or wildcard matching (AliasPattern::Wildcard
). Check AliasPattern for more details.
React
Next.js internally aliases react imports based on context. Reference Webpack config and Turbopack for how it's configured.
ResolvePlugin
ResolveOptionContext
have one another option plugins
, can affect context's resolve result. A plugin implements ResolvePlugin can replace resolved results. Currently resolve plugin only supports after_resolve
, which will be invoked after full resolve completes.
after_resolve
provides some information to determine specific resolve, most notably request
param contains full Request itself.
async fn after_resolve(
&self,
fs_path: Vc<FileSystemPath>,
file_path: Vc<FileSystemPath>,
reference_type: Value<ReferenceType>,
request: Vc<Request>,
) -> Result<Vc<ResolveResultOption>>
Plugin can return its own ResolveResult or return nothing (None). Retuning None will makes original resolve result being honored, otherwise original resolve result will be overridden. In Next.js resolve plugins are being used for triggering side effect by emitting an issue if certain context imports not allowed module (InvalidImportResolvePlugin
), or replace a single import into per-context aware imports (NextNodeSharedRuntimeResolvePlugin
).
Custom transforms
React server components
API references
Turbopack uses code comments and rustdoc to document the API. The full package documentation can be found here.
The list below are packages and brief descriptions that you may want to refer to when first approaching the code.
- napi: NAPI bindings to make Turbopack and SWC's features available in JS code in Next.js
- next-custom-transforms: Collection of SWC transform visitor implementations for features provided by Next.js
- next_api: Linking the interfaces and data structures of the NAPI bindings with the actual Turbopack's next.js feature implementation interfaces
- next_core: Support and implementation of Next.js features in Turbopack
Links
The links below are additional materials such as conference recording. Some of links are not publicly available yet. Ask in team channel if you don't have access.
Writing documentation for Turbopack
Turbopack has two main types of documentation. One is a description of the code and APIs, and the other is a high-level description of the overall behavior. Since there is no way to integrate with rustdoc yet, we deploy mdbook and rustdoc together with a script.
The source for the documentation is available at here.
Write a general overview document
Files written in markdown format in packages/next-swc/docs/src
can be read by mdbook(Like the page you're looking at now). Our configuration includes mermaid support to include diagrams. See architecture how it looks like.
Write a rustdoc comment
If you write a documentation comment in your code, it will be included in the documentation generated by rustdoc. The full index can be found here, and individual packages can be read via https://turbopack-rust-docs.vercel.sh/rustdoc/${pkg_name}/index.html
.
Build the documentation
The scripts/deploy-turbopack-docs.sh
script bundles the mdbook and rustdoc and deploys them. You can use it locally as a build script because it won't run the deployment if it doesn't have a token. The script runs a rustdoc build followed by an mdbook build, and then copies the output of the rustdoc build under mdbook/rustdoc
.
To locally build the documentation, ensure these are installed
- Rust compiler
- mdbook
- mdbook-mermaid
./scripts/deploy-turbopack-docs.sh
open ./target/mdbook/index.html
or
// note you can't check rustdoc links when running devserver
mdbook serve --dest-dir $(pwd)/target/mdbook ./packages/next-swc/docs