OPFS sync write benchmark

2026-09-16T05:08:22.753Z · AMD Ryzen 7 7800X3D 8-Core Processor (16 threads), 65 GB, linux 7.0.0-31-generic x64

Config: preset=quick, N=2000, S=4096 B, chunks=64 KiB / 1 MiB, R=3, M=200 · Profiles on: ext4 (/)

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.

Firefox 158
10 µs100 µs1 ms10 mssmall-appendFirefox 158 — small-append: mean 42.6 µs/call, p50 40.0 µs, p99 80.0 µs, total 85.1 msFirefox 158 — small-append: mean 42.6 µs/call, p50 40.0 µs, p99 80.0 µs, total 85.1 mssmall-overwriteFirefox 158 — small-overwrite: mean 39.4 µs/call, p50 40.0 µs, p99 60.0 µs, total 78.8 msFirefox 158 — small-overwrite: mean 39.4 µs/call, p50 40.0 µs, p99 60.0 µs, total 78.8 mschunked-append (64 KiB)Firefox 158 — chunked-append (64 KiB): mean 54.7 µs/call, p50 60.0 µs, p99 80.0 µs, total 6.8 msFirefox 158 — chunked-append (64 KiB): mean 54.7 µs/call, p50 60.0 µs, p99 80.0 µs, total 6.8 mschunked-append (1 MiB)Firefox 158 — chunked-append (1 MiB): mean 195 µs/call, p50 180 µs, p99 240 µs, total 1.6 msFirefox 158 — chunked-append (1 MiB): mean 195 µs/call, p50 180 µs, p99 240 µs, total 1.6 msappend+flushFirefox 158 — append+flush: mean 5310 µs/call, p50 5580 µs, p99 7520 µs, total 10.6 sFirefox 158 — append+flush: mean 5310 µs/call, p50 5580 µs, p99 7520 µs, total 10.6 s
WorkloadFirefox 158 µs/callp50 / p99 (µs)total
small-append
2000 × write(4096 B) at offset = current size; the file grows on every call
42.6 µs40 / 8085.1 ms
small-overwrite
2000 × write(4096 B) at offset 0 into a file pre-sized to 4096 B; the size never changes
39.4 µs40 / 6078.8 ms
chunked-append (64 KiB)
125 × write(65536 B) appending; the same 8192000 B total as small-append
54.7 µs60 / 806.8 ms
chunked-append (1 MiB)
8 × write(1048576 B) appending; the same 8192000 B total as small-append
195 µs180 / 2401.6 ms
append+flush
small-append with flush() after every write (2000 × 4096 B)
5310 µs5580 / 752010.6 s

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.

Firefox 158
100 µs1 ms10 ms100 msnew-file: create+openFirefox 158 — new-file: create+open: mean 32.4 ms/call, p50 31.4 ms, p99 42.2 ms, total 6.5 sFirefox 158 — new-file: create+open: mean 32.4 ms/call, p50 31.4 ms, p99 42.2 ms, total 6.5 snew-file: first writeFirefox 158 — new-file: first write: mean 109 µs/call, p50 100 µs, p99 160 µs, total 21.9 msFirefox 158 — new-file: first write: mean 109 µs/call, p50 100 µs, p99 160 µs, total 21.9 msnew-file: closeFirefox 158 — new-file: close: mean 21.0 ms/call, p50 20.4 ms, p99 29.6 ms, total 4.2 sFirefox 158 — new-file: close: mean 21.0 ms/call, p50 20.4 ms, p99 29.6 ms, total 4.2 snew-file: deleteFirefox 158 — new-file: delete: mean 21.7 ms/call, p50 21.1 ms, p99 26.5 ms, total 4.3 sFirefox 158 — new-file: delete: mean 21.7 ms/call, p50 21.1 ms, p99 26.5 ms, total 4.3 snew-file: full cycleFirefox 158 — new-file: full cycle: mean 75.2 ms/call, p50 72.9 ms, p99 94.9 ms, total 15.0 sFirefox 158 — new-file: full cycle: mean 75.2 ms/call, p50 72.9 ms, p99 94.9 ms, total 15.0 s
WorkloadFirefox 158 µs/callp50 / p99 (µs)total
new-file: create+open
getFileHandle({create: true}) + createSyncAccessHandle() on a name that does not exist yet
32.4 ms31400 / 421806.5 s
new-file: first write
first write(4096 B, {at: 0}) into the empty file
109 µs100 / 16021.9 ms
new-file: close
close() on the access handle
21.0 ms20360 / 296004.2 s
new-file: delete
removeEntry(name)
21.7 ms21120 / 265204.3 s
new-file: full cycle
create+open → write 4096 B → close → delete, per file
75.2 ms72940 / 9494015.0 s

Environment

BrowserVersionModeProfile onperformance.now() stepIsolatedExecutableUser agent
Firefox 158firefox/158.0a1headlessext4 (/)20 µsyes/home/theo/dev/firefox/obj-opt/dist/bin/firefoxMozilla/5.0 (X11; Linux x86_64; rv:158.0) Gecko/20100101 Firefox/158.0
Firefox 158: Bugzilla summary
**OPFS `FileSystemSyncAccessHandle.write()` per-call overhead benchmark**

Environment:
- User agent: `Mozilla/5.0 (X11; Linux x86_64; rv:158.0) Gecko/20100101 Firefox/158.0`
- Platform: Linux x86_64; hardwareConcurrency: 16
- Timestamp: 2026-09-16T05:08:22.753Z
- performance.now() resolution in the worker: 20 µs (crossOriginIsolated: true)
- navigator.storage.estimate(): 0.5 MB used of 10737 MB
- Config: preset=quick, N=2000, S=4096 B, chunks=64 KiB / 1 MiB, R=3, M=200

Median run of R=3 (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 | 2,000 | 4,096 | 85.1 | 84.1–88.4 | 23,491 | 96.2 | 40.0 | 60.0 | 80.0 | 840.0 | 120.0 |
| small-overwrite | 2,000 | 4,096 | 78.8 | 77.3–78.9 | 25,374 | 103.9 | 40.0 | 40.0 | 60.0 | 80.0 | 80.0 |
| chunked-append (64 KiB) | 125 | 65,536 | 6.8 | 6.7–6.9 | 18,275 | 1197.7 | 60.0 | 60.0 | 80.0 | 140.0 | 140.0 |
| chunked-append (1 MiB) | 8 | 1,048,576 | 1.6 | 1.5–1.6 | 5,128 | 5251.3 | 180.0 | 240.0 | 240.0 | 240.0 | 240.0 |
| append+flush | 2,000 | 4,096 | 10620.8 | 10426.6–10689.3 | 188 | 0.8 | 5580.0 | 5920.0 | 7520.0 | 15140.0 | 4700.0 |
| new-file: create+open | 200 | – | 6479.3 | 6475.5–6551.8 | 31 | – | 31400.0 | 35760.0 | 42180.0 | 50500.0 | 39940.0 |
| new-file: first write | 200 | 4,096 | 21.9 | 21.8–22.7 | 9,149 | 37.5 | 100.0 | 140.0 | 160.0 | 160.0 | 120.0 |
| new-file: close | 200 | – | 4198.3 | 4185.5–4238.0 | 48 | – | 20360.0 | 22340.0 | 29600.0 | 30340.0 | 22340.0 |
| new-file: delete | 200 | – | 4342.0 | 4321.8–4353.7 | 46 | – | 21120.0 | 23200.0 | 26520.0 | 30160.0 | 20940.0 |
| new-file: full cycle | 200 | 4,096 | 15037.5 | 15023.3–15154.4 | 13 | 0.1 | 72940.0 | 79040.0 | 94940.0 | 120320.0 | 94940.0 |

Derived:
- Fixed per-call floor (small-overwrite, size never changes): 39.4 µs/call mean, 40.0 µs p50
- Size-growing write (small-append): 42.6 µs/call mean, 40.0 µs p50
- Extra cost per call when the write grows the file (append − overwrite): 3.2 µs mean, 0.0 µs p50
- Same 8.2 MB in 125 × 64 KiB calls: 6.8 ms (1198 MB/s) vs 85.1 ms for 2,000 × 4 KiB; the small calls are 12.4× slower for the same bytes
- Same 8.2 MB in 8 × 1 MiB calls: 1.6 ms (5251 MB/s) vs 85.1 ms for 2,000 × 4 KiB; the small calls are 54.6× slower for the same bytes
- flush() after every write adds 5267.8 µs/call on top of small-append (append+flush: 5310.4 µs/call mean)
- small-append: first call into the fresh file 120.0 µs vs p50 40.0 / p99 80.0 µs; across runs 80.0–140.0 µs
- chunked-append (64 KiB): first call into the fresh file 140.0 µs vs p50 60.0 / p99 80.0 µs; across runs 120.0–160.0 µs
- chunked-append (1 MiB): first call into the fresh file 240.0 µs vs p50 180.0 / p99 240.0 µs; across runs 220.0–280.0 µs
- append+flush: first call into the fresh file 4700.0 µs vs p50 5580.0 / p99 7520.0 µs; across runs 4700.0–5680.0 µs
- new-file: 75.2 ms per file (13 files/s) = create+open 32.4 + first write 0.1 + close 21.0 + delete 21.7 ms
- new-file without delete (what populating OPFS pays per file): 53.5 ms
- First write into a new file (4 KiB): 109.3 µs vs 42.6 µs per small-append call