Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/01-app/01-getting-started/08-caching.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,6 @@ See [ISR with Cache Components](/docs/app/guides/incremental-static-regeneration

## Bots and crawlers

Browsers receive the static shell instantly. Bots and crawlers are detected by their user agent and handled differently: because they need a complete document, Next.js skips the shell and renders the entire page dynamically at request time, then sends the finished HTML once the render completes.
With [Partial Prerendering](#prerendering), metadata can be dynamic while the rest of the page is prerendered into a static shell. The shell is served immediately and the metadata streams in after. Some bots and crawlers cannot handle this, and need to see the metadata included in the `<head>` tag of the document. Next.js detects them by their user agent, skips the static shell, and renders the page dynamically at request time.

Because the shell is re-rendered instead of reused, work that completed during prerendering now runs at request time for a bot. If part of your shell depends on inputs that only exist while prerendering, such as build-time data or values that are not reachable in the request-time environment, a page that loads for a person can fail to render for a crawler. Make sure the data your shell relies on is also available at request time. See [Bots and crawlers](/docs/app/guides/streaming#bots-and-crawlers) in the Streaming guide for more details.
Data sources available during the build-time prerender have to be available at request time too. Otherwise the render may throw, and these bots receive a 500 error. See [Bots and crawlers](/docs/app/guides/streaming#bots-and-crawlers) in the Streaming guide for which user agents this covers and how to change the list.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { isStableBuild } from './canary-only-config-error'

describe('isStableBuild', () => {
afterEach(() => {
delete process.env.__NEXT_VERSION
delete process.env.__NEXT_TEST_MODE
delete process.env.NEXT_PRIVATE_LOCAL_DEV
})

it.each([
// A plain release version.
{ version: '16.3.0', expected: true },
// A numbered preview release published to npm (`next@preview`).
{ version: '16.3.0-preview.10', expected: true },
// A commit preview tarball (scripts/set-preview-version.js) is built from
// an arbitrary canary commit and must not be treated as stable.
{ version: '16.4.0-preview-84cee7e6-20260917', expected: false },
{ version: '16.4.0-canary.5', expected: false },
])('returns $expected for version $version', ({ version, expected }) => {
process.env.__NEXT_VERSION = version
expect(isStableBuild()).toBe(expected)
})

it('returns true when no version is set', () => {
delete process.env.__NEXT_VERSION
expect(isStableBuild()).toBe(true)
})

it('returns false in test mode even with a stable version', () => {
process.env.__NEXT_VERSION = '16.3.0'
process.env.__NEXT_TEST_MODE = '1'
expect(isStableBuild()).toBe(false)
})

it('returns false for local dev even with a stable version', () => {
process.env.__NEXT_VERSION = '16.3.0'
process.env.NEXT_PRIVATE_LOCAL_DEV = '1'
expect(isStableBuild()).toBe(false)
})
})
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
export function isStableBuild() {
const nextVersion = process.env.__NEXT_VERSION
return (
!process.env.__NEXT_VERSION?.includes('canary') &&
!nextVersion?.includes('canary') &&
// Commit preview tarballs (e.g. `16.4.0-preview-84cee7e6-20260917`, see
// scripts/set-preview-version.js) are built from arbitrary canary commits,
// so they are not stable. Numbered preview releases published to npm
// (e.g. `16.3.0-preview.10`) are stable.
!nextVersion?.includes('-preview-') &&
!process.env.__NEXT_TEST_MODE &&
!process.env.NEXT_PRIVATE_LOCAL_DEV
)
Expand Down
9 changes: 7 additions & 2 deletions turbopack/crates/turbo-persistence/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,11 @@ The checksum is verified on the compressed data **before** decompression when th

## Reading

Reading start from the current sequence number and goes downwards.
Opened meta files are stored in per-family shards. A lookup scans only the requested family's meta
files, from newest to oldest; there is no ordering dependency between families.

- We have all SST files memory mapped
- for i = CURRENT sequence number .. 0
- for each meta file of the queried key family, newest first
- Check AMQF from SST file for key existence -> if not continue
- let block = 0
- loop
Expand Down Expand Up @@ -325,6 +326,10 @@ Since the process might exit unexpectedly, to avoid "forgetting" to delete the S

We limit the number of SST files that are merged at once to avoid long compactions.

Compaction keeps meta files incremental: a new meta file only describes SST files that were merged
or moved. Metadata for untouched SST files stays in its existing meta file. When every active entry
in an old meta file is superseded, that meta file is retired in the same compaction commit.

Full example:

Example:
Expand Down
79 changes: 78 additions & 1 deletion turbopack/crates/turbo-persistence/benches/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,83 @@ fn bench_read_get_multiple(c: &mut Criterion) {
// Compaction Benchmarks
// =============================================================================

fn family_benchmark_key(family: u32, commit: u32, item: u32) -> [u8; 12] {
let mut key = [0; 12];
key[..4].copy_from_slice(&family.to_be_bytes());
key[4..8].copy_from_slice(&commit.to_be_bytes());
key[8..].copy_from_slice(&item.to_be_bytes());
key
}

fn bench_family_sharding(c: &mut Criterion) {
const FAMILIES: usize = 4;
const COMMITS: u32 = 100;
let entries_per_commit = scaled(1_000);
let db = LazyLock::new(|| {
let tempdir = tempfile::tempdir().unwrap();
let config = TpDbConfig {
family_configs: ["family-0", "family-1", "family-2", "family-3"].map(|name| {
FamilyConfig {
name,
kind: FamilyKind::SingleValue,
compression: Compression::Lz4,
}
}),
..TpDbConfig::new()
};
let db = TurboPersistence::<SerialScheduler, FAMILIES>::open_with_config(
tempdir.path().to_path_buf(),
config,
)
.unwrap();
for commit in 0..COMMITS {
for family in 0..FAMILIES as u32 {
let batch = db.write_batch().unwrap();
for item in 0..entries_per_commit as u32 {
batch
.put(
family,
family_benchmark_key(family, commit, item),
item.to_be_bytes().to_vec().into(),
)
.unwrap();
}
db.commit_write_batch(batch).unwrap();
}
}
(tempdir, db)
});

let mut group = c.benchmark_group("read/family_sharding");
group.measurement_time(Duration::from_secs(5));
let hit = family_benchmark_key(2, COMMITS - 1, 0);
let miss = family_benchmark_key(2, COMMITS, 0);
let batch_hits = (0..64)
.map(|item| family_benchmark_key(2, COMMITS - 1, item))
.collect::<Vec<_>>();
let batch_misses = (0..64)
.map(|item| family_benchmark_key(2, COMMITS, item))
.collect::<Vec<_>>();

group.bench_function("get/hit", |b| {
let (_, db) = &*db;
b.iter(|| black_box(db.get(2, black_box(&hit)).unwrap()))
});
group.bench_function("get/miss", |b| {
let (_, db) = &*db;
b.iter(|| black_box(db.get(2, black_box(&miss)).unwrap()))
});
group.bench_function("batch_get/hit_64", |b| {
let (_, db) = &*db;
b.iter(|| black_box(db.batch_get(2, black_box(&batch_hits)).unwrap()))
});
group.bench_function("batch_get/miss_64", |b| {
let (_, db) = &*db;
b.iter(|| black_box(db.batch_get(2, black_box(&batch_misses)).unwrap()))
});
group.finish();
}

fn bench_compaction(c: &mut Criterion) {
let mut group = c.benchmark_group("compaction");
// Compaction is expensive, reduce sample size
Expand Down Expand Up @@ -1499,6 +1576,6 @@ fn bench_block_cache(c: &mut Criterion) {
criterion_group!(
name = benches;
config = Criterion::default();
targets = bench_write, bench_write_multi_value, bench_read_get, bench_read_batch_get, bench_read_get_multiple, bench_compaction, bench_compaction_multi_value, bench_qfilter, bench_static_sorted_file_lookup, bench_block_cache
targets = bench_write, bench_write_multi_value, bench_read_get, bench_read_batch_get, bench_read_get_multiple, bench_family_sharding, bench_compaction, bench_compaction_multi_value, bench_qfilter, bench_static_sorted_file_lookup, bench_block_cache
);
criterion_main!(benches);
Loading
Loading