OPFS sync write benchmark
2026-09-16T03:17:46.590Z · AMD Ryzen 7 7800X3D 8-Core Processor (16 threads), 65 GB, linux 7.0.0-31-generic x64
Config: preset=full, N=20000, S=4096 B, chunks=64 KiB / 1 MiB, R=5, M=2000 · Profiles on: tmpfs (/tmp)
- Note: Control run with the browser profiles on tmpfs: fsync is free there, so durability-bound rows show the CPU and IPC floor
Per-call write cost
Workloads 1–4. small-overwrite is the fixed per-call floor; small-append adds the cost of growing the file; chunked-append writes the same bytes in far fewer calls; append+flush is a control, since both browsers flush per call. Mean time per call in the median run, log scale; the distance between the marks is the ratio between browsers, printed on the right. Hover a mark for p50 and p99.
| Workload | Chrome 152 µs/call | p50 / p99 (µs) | total | Firefox 155 µs/call | p50 / p99 (µs) | total | Firefox 155 ÷ Chrome 152 |
|---|---|---|---|---|---|---|---|
| small-append 20000 × write(4096 B) at offset = current size; the file grows on every call | 5.5 µs | 5 / 10 | 109.2 ms | 33.6 µs | 40 / 40 | 672.7 ms | 6.2× |
| small-overwrite 20000 × write(4096 B) at offset 0 into a file pre-sized to 4096 B; the size never changes | 4.2 µs | 5 / 10 | 84.1 ms | 32.4 µs | 40 / 40 | 647.2 ms | 7.7× |
| chunked-append (64 KiB) 1250 × write(65536 B) appending; the same 81920000 B total as small-append | 22.1 µs | 20 / 65 | 27.6 ms | 51.1 µs | 60 / 80 | 63.8 ms | 2.3× |
| chunked-append (1 MiB) 79 × write(1048576 B) appending; the same 81920000 B total as small-append | 283 µs | 275 / 455 | 22.4 ms | 315 µs | 320 / 440 | 24.9 ms | 1.1× |
| append+flush small-append with flush() after every write (20000 × 4096 B) | 5.9 µs | 5 / 10 | 117.3 ms | 42.3 µs | 40 / 60 | 846.3 ms | 7.2× |
Per-file cost
Workload 5. Each step of create+open, first write, close and delete timed on its own, per file, plus the full cycle. Populating a file system pays the first three per file. Mean time per call in the median run, log scale; the distance between the marks is the ratio between browsers, printed on the right. Hover a mark for p50 and p99.
| Workload | Chrome 152 µs/call | p50 / p99 (µs) | total | Firefox 155 µs/call | p50 / p99 (µs) | total | Firefox 155 ÷ Chrome 152 |
|---|---|---|---|---|---|---|---|
| new-file: create+open getFileHandle({create: true}) + createSyncAccessHandle() on a name that does not exist yet | 309 µs | 305 / 395 | 618.3 ms | 592 µs | 580 / 760 | 1.2 s | 1.9× |
| new-file: first write first write(4096 B, {at: 0}) into the empty file | 73.7 µs | 70 / 110 | 147.4 ms | 55.7 µs | 60 / 80 | 111.3 ms | 0.8× |
| new-file: close close() on the access handle | 78.7 µs | 75 / 110 | 157.4 ms | 236 µs | 220 / 320 | 472.6 ms | 3.0× |
| new-file: delete removeEntry(name) | 112 µs | 105 / 145 | 224.2 ms | 449 µs | 420 / 600 | 898.2 ms | 4.0× |
| new-file: full cycle create+open → write 4096 B → close → delete, per file | 573 µs | 560 / 710 | 1.1 s | 1337 µs | 1280 / 1660 | 2.7 s | 2.3× |
Environment
| Browser | Version | Mode | Profile on | performance.now() step | Isolated | Executable | User agent |
|---|---|---|---|---|---|---|---|
| Chrome 152 | Chrome/152.0.7977.82 | headless | tmpfs (/tmp) | 5 µs | yes | /usr/bin/google-chrome | Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36 |
| Firefox 155 | firefox/155.0.1 | headless | tmpfs (/tmp) | 20 µs | yes | /home/theo/.cache/puppeteer/firefox/linux-stable_155.0.1/firefox/firefox | Mozilla/5.0 (X11; Linux x86_64; rv:155.0) Gecko/20100101 Firefox/155.0 |
Chrome 152: Bugzilla summary
**OPFS `FileSystemSyncAccessHandle.write()` per-call overhead benchmark** Environment: - User agent: `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36` - Platform: Linux x86_64; hardwareConcurrency: 16 - Timestamp: 2026-09-16T03:17:46.590Z - performance.now() resolution in the worker: 5.0 µs (crossOriginIsolated: true) - navigator.storage.estimate(): 0.0 MB used of 10737 MB - Config: preset=full, N=20000, S=4096 B, chunks=64 KiB / 1 MiB, R=5, M=2000 Median run of R=5 (by total time) after one warm-up pass; latency percentiles are per call; "1st call" is call number 1 of that run (for the append workloads, the first write into the freshly created file): | Workload | Calls | Bytes/call | Total (ms) | Total min–max (ms) | ops/s | MB/s | p50 (µs) | p90 (µs) | p99 (µs) | max (µs) | 1st call (µs) | |---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| | small-append | 20,000 | 4,096 | 109.2 | 108.3–112.2 | 183,184 | 750.3 | 5.0 | 10.0 | 10.0 | 110.0 | 100.0 | | small-overwrite | 20,000 | 4,096 | 84.1 | 83.8–84.4 | 237,699 | 973.6 | 5.0 | 5.0 | 10.0 | 85.0 | 5.0 | | chunked-append (64 KiB) | 1,250 | 65,536 | 27.6 | 27.0–29.6 | 45,339 | 2971.3 | 20.0 | 25.0 | 65.0 | 145.0 | 145.0 | | chunked-append (1 MiB) | 79 | 1,048,576 | 22.4 | 21.9–22.9 | 3,528 | 3658.8 | 275.0 | 345.0 | 455.0 | 455.0 | 455.0 | | append+flush | 20,000 | 4,096 | 117.3 | 116.9–119.2 | 170,503 | 698.4 | 5.0 | 10.0 | 10.0 | 105.0 | 100.0 | | new-file: create+open | 2,000 | – | 618.3 | 608.5–691.3 | 3,235 | – | 305.0 | 340.0 | 395.0 | 1135.0 | 415.0 | | new-file: first write | 2,000 | 4,096 | 147.4 | 145.5–154.6 | 13,564 | 55.6 | 70.0 | 85.0 | 110.0 | 140.0 | 100.0 | | new-file: close | 2,000 | – | 157.4 | 153.4–161.0 | 12,705 | – | 75.0 | 90.0 | 110.0 | 140.0 | 80.0 | | new-file: delete | 2,000 | – | 224.2 | 216.1–233.3 | 8,921 | – | 105.0 | 120.0 | 145.0 | 9355.0 | 140.0 | | new-file: full cycle | 2,000 | 4,096 | 1145.1 | 1124.9–1236.1 | 1,747 | 7.2 | 560.0 | 615.0 | 710.0 | 9810.0 | 750.0 | Derived: - Fixed per-call floor (small-overwrite, size never changes): 4.2 µs/call mean, 5.0 µs p50 - Size-growing write (small-append): 5.5 µs/call mean, 5.0 µs p50 - Extra cost per call when the write grows the file (append − overwrite): 1.3 µs mean, 0.0 µs p50 - Same 82 MB in 1,250 × 64 KiB calls: 27.6 ms (2971 MB/s) vs 109.2 ms for 20,000 × 4 KiB; the small calls are 4.0× slower for the same bytes - Same 82 MB in 79 × 1 MiB calls: 22.4 ms (3659 MB/s) vs 109.2 ms for 20,000 × 4 KiB; the small calls are 4.9× slower for the same bytes - flush() after every write adds 0.4 µs/call on top of small-append (append+flush: 5.9 µs/call mean) - small-append: first call into the fresh file 100.0 µs vs p50 5.0 / p99 10.0 µs; across runs 90.0–140.0 µs (far above p99) - chunked-append (64 KiB): first call into the fresh file 145.0 µs vs p50 20.0 / p99 65.0 µs; across runs 105.0–160.0 µs - chunked-append (1 MiB): first call into the fresh file 455.0 µs vs p50 275.0 / p99 455.0 µs; across runs 400.0–485.0 µs - append+flush: first call into the fresh file 100.0 µs vs p50 5.0 / p99 10.0 µs; across runs 75.0–150.0 µs (far above p99) - new-file: 0.6 ms per file (1,747 files/s) = create+open 0.3 + first write 0.1 + close 0.1 + delete 0.1 ms - new-file without delete (what populating OPFS pays per file): 0.5 ms - First write into a new file (4 KiB): 73.7 µs vs 5.5 µs per small-append call
Firefox 155: Bugzilla summary
**OPFS `FileSystemSyncAccessHandle.write()` per-call overhead benchmark** Environment: - User agent: `Mozilla/5.0 (X11; Linux x86_64; rv:155.0) Gecko/20100101 Firefox/155.0` - Platform: Linux x86_64; hardwareConcurrency: 16 - Timestamp: 2026-09-16T03:17:59.132Z - performance.now() resolution in the worker: 20 µs (crossOriginIsolated: true) - navigator.storage.estimate(): 0.5 MB used of 3269 MB - Config: preset=full, N=20000, S=4096 B, chunks=64 KiB / 1 MiB, R=5, M=2000 Median run of R=5 (by total time) after one warm-up pass; latency percentiles are per call; "1st call" is call number 1 of that run (for the append workloads, the first write into the freshly created file): | Workload | Calls | Bytes/call | Total (ms) | Total min–max (ms) | ops/s | MB/s | p50 (µs) | p90 (µs) | p99 (µs) | max (µs) | 1st call (µs) | |---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| | small-append | 20,000 | 4,096 | 672.7 | 671.8–694.8 | 29,732 | 121.8 | 40.0 | 40.0 | 40.0 | 1100.0 | 60.0 | | small-overwrite | 20,000 | 4,096 | 647.2 | 634.3–654.2 | 30,903 | 126.6 | 40.0 | 40.0 | 40.0 | 80.0 | 40.0 | | chunked-append (64 KiB) | 1,250 | 65,536 | 63.8 | 62.2–66.0 | 19,580 | 1283.2 | 60.0 | 60.0 | 80.0 | 100.0 | 80.0 | | chunked-append (1 MiB) | 79 | 1,048,576 | 24.9 | 23.3–26.3 | 3,170 | 3287.3 | 320.0 | 340.0 | 440.0 | 440.0 | 440.0 | | append+flush | 20,000 | 4,096 | 846.3 | 844.9–859.1 | 23,632 | 96.8 | 40.0 | 60.0 | 60.0 | 100.0 | 80.0 | | new-file: create+open | 2,000 | – | 1184.0 | 1169.9–1240.8 | 1,689 | – | 580.0 | 680.0 | 760.0 | 840.0 | 620.0 | | new-file: first write | 2,000 | 4,096 | 111.3 | 110.1–117.8 | 17,963 | 73.6 | 60.0 | 60.0 | 80.0 | 180.0 | 80.0 | | new-file: close | 2,000 | – | 472.6 | 471.4–486.1 | 4,232 | – | 220.0 | 280.0 | 320.0 | 380.0 | 340.0 | | new-file: delete | 2,000 | – | 898.2 | 891.3–929.7 | 2,227 | – | 420.0 | 540.0 | 600.0 | 680.0 | 460.0 | | new-file: full cycle | 2,000 | 4,096 | 2674.2 | 2650.9–2776.2 | 748 | 3.1 | 1280.0 | 1460.0 | 1660.0 | 28220.0 | 1420.0 | Derived: - Fixed per-call floor (small-overwrite, size never changes): 32.4 µs/call mean, 40.0 µs p50 - Size-growing write (small-append): 33.6 µs/call mean, 40.0 µs p50 - Extra cost per call when the write grows the file (append − overwrite): 1.3 µs mean, 0.0 µs p50 - Same 82 MB in 1,250 × 64 KiB calls: 63.8 ms (1283 MB/s) vs 672.7 ms for 20,000 × 4 KiB; the small calls are 10.5× slower for the same bytes - Same 82 MB in 79 × 1 MiB calls: 24.9 ms (3287 MB/s) vs 672.7 ms for 20,000 × 4 KiB; the small calls are 27.0× slower for the same bytes - flush() after every write adds 8.7 µs/call on top of small-append (append+flush: 42.3 µs/call mean) - small-append: first call into the fresh file 60.0 µs vs p50 40.0 / p99 40.0 µs; across runs 60.0–100.0 µs - chunked-append (64 KiB): first call into the fresh file 80.0 µs vs p50 60.0 / p99 80.0 µs; across runs 80.0–100.0 µs - chunked-append (1 MiB): first call into the fresh file 440.0 µs vs p50 320.0 / p99 440.0 µs; across runs 320.0–440.0 µs - append+flush: first call into the fresh file 80.0 µs vs p50 40.0 / p99 60.0 µs; across runs 80.0–100.0 µs - new-file: 1.3 ms per file (748 files/s) = create+open 0.6 + first write 0.1 + close 0.2 + delete 0.4 ms - new-file without delete (what populating OPFS pays per file): 0.9 ms - First write into a new file (4 KiB): 55.7 µs vs 33.6 µs per small-append call