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)

Experiment, not a cross-browser comparison.

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.

Chrome 152Firefox 155
1 µs10 µs100 µs1 mssmall-appendChrome 152 — small-append: mean 5.5 µs/call, p50 5.0 µs, p99 10.0 µs, total 109.2 msChrome 152 — small-append: mean 5.5 µs/call, p50 5.0 µs, p99 10.0 µs, total 109.2 msFirefox 155 — small-append: mean 33.6 µs/call, p50 40.0 µs, p99 40.0 µs, total 672.7 msFirefox 155 — small-append: mean 33.6 µs/call, p50 40.0 µs, p99 40.0 µs, total 672.7 ms6.2×small-overwriteChrome 152 — small-overwrite: mean 4.2 µs/call, p50 5.0 µs, p99 10.0 µs, total 84.1 msChrome 152 — small-overwrite: mean 4.2 µs/call, p50 5.0 µs, p99 10.0 µs, total 84.1 msFirefox 155 — small-overwrite: mean 32.4 µs/call, p50 40.0 µs, p99 40.0 µs, total 647.2 msFirefox 155 — small-overwrite: mean 32.4 µs/call, p50 40.0 µs, p99 40.0 µs, total 647.2 ms7.7×chunked-append (64 KiB)Chrome 152 — chunked-append (64 KiB): mean 22.1 µs/call, p50 20.0 µs, p99 65.0 µs, total 27.6 msChrome 152 — chunked-append (64 KiB): mean 22.1 µs/call, p50 20.0 µs, p99 65.0 µs, total 27.6 msFirefox 155 — chunked-append (64 KiB): mean 51.1 µs/call, p50 60.0 µs, p99 80.0 µs, total 63.8 msFirefox 155 — chunked-append (64 KiB): mean 51.1 µs/call, p50 60.0 µs, p99 80.0 µs, total 63.8 ms2.3×chunked-append (1 MiB)Chrome 152 — chunked-append (1 MiB): mean 283 µs/call, p50 275 µs, p99 455 µs, total 22.4 msChrome 152 — chunked-append (1 MiB): mean 283 µs/call, p50 275 µs, p99 455 µs, total 22.4 msFirefox 155 — chunked-append (1 MiB): mean 315 µs/call, p50 320 µs, p99 440 µs, total 24.9 msFirefox 155 — chunked-append (1 MiB): mean 315 µs/call, p50 320 µs, p99 440 µs, total 24.9 ms1.1×append+flushChrome 152 — append+flush: mean 5.9 µs/call, p50 5.0 µs, p99 10.0 µs, total 117.3 msChrome 152 — append+flush: mean 5.9 µs/call, p50 5.0 µs, p99 10.0 µs, total 117.3 msFirefox 155 — append+flush: mean 42.3 µs/call, p50 40.0 µs, p99 60.0 µs, total 846.3 msFirefox 155 — append+flush: mean 42.3 µs/call, p50 40.0 µs, p99 60.0 µs, total 846.3 ms7.2×Firefox 155 ÷ Chrome 152
WorkloadChrome 152 µs/callp50 / p99 (µs)totalFirefox 155 µs/callp50 / p99 (µs)totalFirefox 155 ÷ Chrome 152
small-append
20000 × write(4096 B) at offset = current size; the file grows on every call
5.5 µs5 / 10109.2 ms33.6 µs40 / 40672.7 ms6.2×
small-overwrite
20000 × write(4096 B) at offset 0 into a file pre-sized to 4096 B; the size never changes
4.2 µs5 / 1084.1 ms32.4 µs40 / 40647.2 ms7.7×
chunked-append (64 KiB)
1250 × write(65536 B) appending; the same 81920000 B total as small-append
22.1 µs20 / 6527.6 ms51.1 µs60 / 8063.8 ms2.3×
chunked-append (1 MiB)
79 × write(1048576 B) appending; the same 81920000 B total as small-append
283 µs275 / 45522.4 ms315 µs320 / 44024.9 ms1.1×
append+flush
small-append with flush() after every write (20000 × 4096 B)
5.9 µs5 / 10117.3 ms42.3 µs40 / 60846.3 ms7.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.

Chrome 152Firefox 155
10 µs100 µs1 ms10 msnew-file: create+openChrome 152 — new-file: create+open: mean 309 µs/call, p50 305 µs, p99 395 µs, total 618.3 msChrome 152 — new-file: create+open: mean 309 µs/call, p50 305 µs, p99 395 µs, total 618.3 msFirefox 155 — new-file: create+open: mean 592 µs/call, p50 580 µs, p99 760 µs, total 1.2 sFirefox 155 — new-file: create+open: mean 592 µs/call, p50 580 µs, p99 760 µs, total 1.2 s1.9×new-file: first writeChrome 152 — new-file: first write: mean 73.7 µs/call, p50 70.0 µs, p99 110 µs, total 147.4 msChrome 152 — new-file: first write: mean 73.7 µs/call, p50 70.0 µs, p99 110 µs, total 147.4 msFirefox 155 — new-file: first write: mean 55.7 µs/call, p50 60.0 µs, p99 80.0 µs, total 111.3 msFirefox 155 — new-file: first write: mean 55.7 µs/call, p50 60.0 µs, p99 80.0 µs, total 111.3 ms0.8×new-file: closeChrome 152 — new-file: close: mean 78.7 µs/call, p50 75.0 µs, p99 110 µs, total 157.4 msChrome 152 — new-file: close: mean 78.7 µs/call, p50 75.0 µs, p99 110 µs, total 157.4 msFirefox 155 — new-file: close: mean 236 µs/call, p50 220 µs, p99 320 µs, total 472.6 msFirefox 155 — new-file: close: mean 236 µs/call, p50 220 µs, p99 320 µs, total 472.6 ms3.0×new-file: deleteChrome 152 — new-file: delete: mean 112 µs/call, p50 105 µs, p99 145 µs, total 224.2 msChrome 152 — new-file: delete: mean 112 µs/call, p50 105 µs, p99 145 µs, total 224.2 msFirefox 155 — new-file: delete: mean 449 µs/call, p50 420 µs, p99 600 µs, total 898.2 msFirefox 155 — new-file: delete: mean 449 µs/call, p50 420 µs, p99 600 µs, total 898.2 ms4.0×new-file: full cycleChrome 152 — new-file: full cycle: mean 573 µs/call, p50 560 µs, p99 710 µs, total 1.1 sChrome 152 — new-file: full cycle: mean 573 µs/call, p50 560 µs, p99 710 µs, total 1.1 sFirefox 155 — new-file: full cycle: mean 1337 µs/call, p50 1280 µs, p99 1660 µs, total 2.7 sFirefox 155 — new-file: full cycle: mean 1337 µs/call, p50 1280 µs, p99 1660 µs, total 2.7 s2.3×Firefox 155 ÷ Chrome 152
WorkloadChrome 152 µs/callp50 / p99 (µs)totalFirefox 155 µs/callp50 / p99 (µs)totalFirefox 155 ÷ Chrome 152
new-file: create+open
getFileHandle({create: true}) + createSyncAccessHandle() on a name that does not exist yet
309 µs305 / 395618.3 ms592 µs580 / 7601.2 s1.9×
new-file: first write
first write(4096 B, {at: 0}) into the empty file
73.7 µs70 / 110147.4 ms55.7 µs60 / 80111.3 ms0.8×
new-file: close
close() on the access handle
78.7 µs75 / 110157.4 ms236 µs220 / 320472.6 ms3.0×
new-file: delete
removeEntry(name)
112 µs105 / 145224.2 ms449 µs420 / 600898.2 ms4.0×
new-file: full cycle
create+open → write 4096 B → close → delete, per file
573 µs560 / 7101.1 s1337 µs1280 / 16602.7 s2.3×

Environment

BrowserVersionModeProfile onperformance.now() stepIsolatedExecutableUser agent
Chrome 152Chrome/152.0.7977.82headlesstmpfs (/tmp)5 µsyes/usr/bin/google-chromeMozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36
Firefox 155firefox/155.0.1headlesstmpfs (/tmp)20 µsyes/home/theo/.cache/puppeteer/firefox/linux-stable_155.0.1/firefox/firefoxMozilla/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