OPFS sync write benchmark

2026-09-16T03:20:09.457Z · 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.

Chrome 152Firefox 155
10 µs100 µs1 ms10 mssmall-appendChrome 152 — small-append: mean 25.5 µs/call, p50 10.0 µs, p99 15.0 µs, total 51.1 msChrome 152 — small-append: mean 25.5 µs/call, p50 10.0 µs, p99 15.0 µs, total 51.1 msFirefox 155 — small-append: mean 287 µs/call, p50 280 µs, p99 400 µs, total 573.5 msFirefox 155 — small-append: mean 287 µs/call, p50 280 µs, p99 400 µs, total 573.5 ms11.2×small-overwriteChrome 152 — small-overwrite: mean 12.7 µs/call, p50 10.0 µs, p99 15.0 µs, total 25.3 msChrome 152 — small-overwrite: mean 12.7 µs/call, p50 10.0 µs, p99 15.0 µs, total 25.3 msFirefox 155 — small-overwrite: mean 283 µs/call, p50 280 µs, p99 340 µs, total 566.9 msFirefox 155 — small-overwrite: mean 283 µs/call, p50 280 µs, p99 340 µs, total 566.9 ms22.4×chunked-append (64 KiB)Chrome 152 — chunked-append (64 KiB): mean 46.9 µs/call, p50 20.0 µs, p99 900 µs, total 5.9 msChrome 152 — chunked-append (64 KiB): mean 46.9 µs/call, p50 20.0 µs, p99 900 µs, total 5.9 msFirefox 155 — chunked-append (64 KiB): mean 293 µs/call, p50 300 µs, p99 360 µs, total 36.7 msFirefox 155 — chunked-append (64 KiB): mean 293 µs/call, p50 300 µs, p99 360 µs, total 36.7 ms6.3×chunked-append (1 MiB)Chrome 152 — chunked-append (1 MiB): mean 366 µs/call, p50 160 µs, p99 670 µs, total 2.9 msChrome 152 — chunked-append (1 MiB): mean 366 µs/call, p50 160 µs, p99 670 µs, total 2.9 msFirefox 155 — chunked-append (1 MiB): mean 438 µs/call, p50 420 µs, p99 460 µs, total 3.5 msFirefox 155 — chunked-append (1 MiB): mean 438 µs/call, p50 420 µs, p99 460 µs, total 3.5 ms1.2×append+flushChrome 152 — append+flush: mean 5142 µs/call, p50 5490 µs, p99 6290 µs, total 10.3 sChrome 152 — append+flush: mean 5142 µs/call, p50 5490 µs, p99 6290 µs, total 10.3 sFirefox 155 — append+flush: mean 5949 µs/call, p50 5960 µs, p99 10.5 ms, total 11.9 sFirefox 155 — append+flush: mean 5949 µs/call, p50 5960 µs, p99 10.5 ms, total 11.9 s1.2×Firefox 155 ÷ Chrome 152
WorkloadChrome 152 µs/callp50 / p99 (µs)totalFirefox 155 µs/callp50 / p99 (µs)totalFirefox 155 ÷ Chrome 152
small-append
2000 × write(4096 B) at offset = current size; the file grows on every call
25.5 µs10 / 1551.1 ms287 µs280 / 400573.5 ms11.2×
small-overwrite
2000 × write(4096 B) at offset 0 into a file pre-sized to 4096 B; the size never changes
12.7 µs10 / 1525.3 ms283 µs280 / 340566.9 ms22.4×
chunked-append (64 KiB)
125 × write(65536 B) appending; the same 8192000 B total as small-append
46.9 µs20 / 9005.9 ms293 µs300 / 36036.7 ms6.3×
chunked-append (1 MiB)
8 × write(1048576 B) appending; the same 8192000 B total as small-append
366 µs160 / 6702.9 ms438 µs420 / 4603.5 ms1.2×
append+flush
small-append with flush() after every write (2000 × 4096 B)
5142 µs5490 / 629010.3 s5949 µs5960 / 1054011.9 s1.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
100 µs1 ms10 ms100 ms1000 msnew-file: create+openChrome 152 — new-file: create+open: mean 1693 µs/call, p50 1670 µs, p99 1970 µs, total 338.6 msChrome 152 — new-file: create+open: mean 1693 µs/call, p50 1670 µs, p99 1970 µs, total 338.6 msFirefox 155 — new-file: create+open: mean 49.1 ms/call, p50 47.7 ms, p99 62.0 ms, total 9.8 sFirefox 155 — new-file: create+open: mean 49.1 ms/call, p50 47.7 ms, p99 62.0 ms, total 9.8 s29.0×new-file: first writeChrome 152 — new-file: first write: mean 365 µs/call, p50 360 µs, p99 420 µs, total 73.0 msChrome 152 — new-file: first write: mean 365 µs/call, p50 360 µs, p99 420 µs, total 73.0 msFirefox 155 — new-file: first write: mean 587 µs/call, p50 580 µs, p99 760 µs, total 117.3 msFirefox 155 — new-file: first write: mean 587 µs/call, p50 580 µs, p99 760 µs, total 117.3 ms1.6×new-file: closeChrome 152 — new-file: close: mean 496 µs/call, p50 485 µs, p99 605 µs, total 99.2 msChrome 152 — new-file: close: mean 496 µs/call, p50 485 µs, p99 605 µs, total 99.2 msFirefox 155 — new-file: close: mean 31.9 ms/call, p50 31.2 ms, p99 40.5 ms, total 6.4 sFirefox 155 — new-file: close: mean 31.9 ms/call, p50 31.2 ms, p99 40.5 ms, total 6.4 s64.2×new-file: deleteChrome 152 — new-file: delete: mean 480 µs/call, p50 465 µs, p99 615 µs, total 96.0 msChrome 152 — new-file: delete: mean 480 µs/call, p50 465 µs, p99 615 µs, total 96.0 msFirefox 155 — new-file: delete: mean 32.3 ms/call, p50 31.6 ms, p99 40.4 ms, total 6.5 sFirefox 155 — new-file: delete: mean 32.3 ms/call, p50 31.6 ms, p99 40.4 ms, total 6.5 s67.4×new-file: full cycleChrome 152 — new-file: full cycle: mean 3035 µs/call, p50 3000 µs, p99 3405 µs, total 607.1 msChrome 152 — new-file: full cycle: mean 3035 µs/call, p50 3000 µs, p99 3405 µs, total 607.1 msFirefox 155 — new-file: full cycle: mean 113.9 ms/call, p50 111.1 ms, p99 150.1 ms, total 22.8 sFirefox 155 — new-file: full cycle: mean 113.9 ms/call, p50 111.1 ms, p99 150.1 ms, total 22.8 s37.5×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
1693 µs1670 / 1970338.6 ms49.1 ms47720 / 620209.8 s29.0×
new-file: first write
first write(4096 B, {at: 0}) into the empty file
365 µs360 / 42073.0 ms587 µs580 / 760117.3 ms1.6×
new-file: close
close() on the access handle
496 µs485 / 60599.2 ms31.9 ms31220 / 405206.4 s64.2×
new-file: delete
removeEntry(name)
480 µs465 / 61596.0 ms32.3 ms31580 / 403606.5 s67.4×
new-file: full cycle
create+open → write 4096 B → close → delete, per file
3035 µs3000 / 3405607.1 ms113.9 ms111100 / 15012022.8 s37.5×

Durability syscalls per call (strace)

fsync,fdatasync,syncfs,sync_file_range counted by strace and attributed to each pass's timed loop by wall-clock window; "per call" divides by calls × passes, warm-up included. "Outside the loop" is the per-pass file creation and deletion that workloads 1–4 do around their timed loop; new-file times those steps inside the loop.

WorkloadChrome 152 syncs/callms in sync/calloutside loop, per passFirefox 155 syncs/callms in sync/calloutside loop, per pass
small-append0.000.000.000.010.0926
small-overwrite0.000.000.250.000.0121
chunked-append (64 KiB)0.000.010.500.000.0021
chunked-append (1 MiB)0.000.000.000.000.0021
append+flush1.05.341.01.05.5522
new-file0.000.000.0021111.430.00

Chrome 152: 8351 calls, 44.6 s inside them, 233 outside any workload. Most-synced files:

Firefox 155: 25759 calls, 138.7 s inside them, 311 outside any workload. Most-synced files:

Environment

BrowserVersionModeProfile onperformance.now() stepIsolatedExecutableUser agent
Chrome 152Chrome/152.0.7977.82headless, straceext4 (/)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.1headless, straceext4 (/)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:20:09.457Z
- performance.now() resolution in the worker: 5.0 µs (crossOriginIsolated: true)
- navigator.storage.estimate(): 0.0 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 | 51.1 | 50.4–53.1 | 39,150 | 160.4 | 10.0 | 10.0 | 15.0 | 15750.0 | 385.0 |
| small-overwrite | 2,000 | 4,096 | 25.3 | 24.0–27.0 | 78,958 | 323.4 | 10.0 | 10.0 | 15.0 | 9140.0 | 15.0 |
| chunked-append (64 KiB) | 125 | 65,536 | 5.9 | 5.7–6.1 | 21,313 | 1396.8 | 20.0 | 25.0 | 900.0 | 1220.0 | 390.0 |
| chunked-append (1 MiB) | 8 | 1,048,576 | 2.9 | 2.6–3.3 | 2,735 | 2800.7 | 160.0 | 670.0 | 670.0 | 670.0 | 635.0 |
| append+flush | 2,000 | 4,096 | 10284.4 | 10250.9–11208.2 | 194 | 0.8 | 5490.0 | 5665.0 | 6290.0 | 10075.0 | 5915.0 |
| new-file: create+open | 200 | – | 338.6 | 338.3–338.8 | 591 | – | 1670.0 | 1800.0 | 1970.0 | 2010.0 | 1955.0 |
| new-file: first write | 200 | 4,096 | 73.0 | 72.6–73.7 | 2,738 | 11.2 | 360.0 | 395.0 | 420.0 | 440.0 | 395.0 |
| new-file: close | 200 | – | 99.2 | 98.5–99.4 | 2,015 | – | 485.0 | 535.0 | 605.0 | 620.0 | 530.0 |
| new-file: delete | 200 | – | 96.0 | 95.4–97.2 | 2,083 | – | 465.0 | 525.0 | 615.0 | 620.0 | 445.0 |
| new-file: full cycle | 200 | 4,096 | 607.1 | 606.2–608.3 | 329 | 1.3 | 3000.0 | 3200.0 | 3405.0 | 3990.0 | 2930.0 |

Derived:
- Fixed per-call floor (small-overwrite, size never changes): 12.7 µs/call mean, 10.0 µs p50
- Size-growing write (small-append): 25.5 µs/call mean, 10.0 µs p50
- Extra cost per call when the write grows the file (append − overwrite): 12.9 µs mean, 0.0 µs p50
- Same 8.2 MB in 125 × 64 KiB calls: 5.9 ms (1397 MB/s) vs 51.1 ms for 2,000 × 4 KiB; the small calls are 8.7× slower for the same bytes
- Same 8.2 MB in 8 × 1 MiB calls: 2.9 ms (2801 MB/s) vs 51.1 ms for 2,000 × 4 KiB; the small calls are 17.5× slower for the same bytes
- flush() after every write adds 5116.7 µs/call on top of small-append (append+flush: 5142.2 µs/call mean)
- small-append: first call into the fresh file 385.0 µs vs p50 10.0 / p99 15.0 µs; across runs 340.0–400.0 µs (far above p99)
- chunked-append (64 KiB): first call into the fresh file 390.0 µs vs p50 20.0 / p99 900.0 µs; across runs 355.0–395.0 µs
- chunked-append (1 MiB): first call into the fresh file 635.0 µs vs p50 160.0 / p99 670.0 µs; across runs 495.0–635.0 µs
- append+flush: first call into the fresh file 5915.0 µs vs p50 5490.0 / p99 6290.0 µs; across runs 4925.0–5915.0 µs
- new-file: 3.0 ms per file (329 files/s) = create+open 1.7 + first write 0.4 + close 0.5 + delete 0.5 ms
- new-file without delete (what populating OPFS pays per file): 2.6 ms
- First write into a new file (4 KiB): 365.2 µs vs 25.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:20:58.645Z
- 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 | 573.5 | 545.6–578.2 | 3,487 | 14.3 | 280.0 | 300.0 | 400.0 | 1100.0 | 520.0 |
| small-overwrite | 2,000 | 4,096 | 566.9 | 551.4–606.2 | 3,528 | 14.5 | 280.0 | 300.0 | 340.0 | 780.0 | 340.0 |
| chunked-append (64 KiB) | 125 | 65,536 | 36.7 | 34.9–37.9 | 3,408 | 223.3 | 300.0 | 300.0 | 360.0 | 440.0 | 440.0 |
| chunked-append (1 MiB) | 8 | 1,048,576 | 3.5 | 3.4–3.6 | 2,286 | 2340.6 | 420.0 | 460.0 | 460.0 | 460.0 | 420.0 |
| append+flush | 2,000 | 4,096 | 11898.4 | 11227.0–12248.6 | 168 | 0.7 | 5960.0 | 7160.0 | 10540.0 | 15440.0 | 6960.0 |
| new-file: create+open | 200 | – | 9818.6 | 9639.7–10752.1 | 20 | – | 47720.0 | 50620.0 | 62020.0 | 115080.0 | 58620.0 |
| new-file: first write | 200 | 4,096 | 117.3 | 113.5–117.6 | 1,705 | 7.0 | 580.0 | 640.0 | 760.0 | 1440.0 | 620.0 |
| new-file: close | 200 | – | 6376.7 | 6316.2–7102.7 | 31 | – | 31220.0 | 32560.0 | 40520.0 | 41400.0 | 40800.0 |
| new-file: delete | 200 | – | 6467.9 | 6408.4–7155.2 | 31 | – | 31580.0 | 33040.0 | 40360.0 | 60900.0 | 39860.0 |
| new-file: full cycle | 200 | 4,096 | 22777.8 | 22482.5–25128.6 | 9 | 0.0 | 111100.0 | 117000.0 | 150120.0 | 177800.0 | 139840.0 |

Derived:
- Fixed per-call floor (small-overwrite, size never changes): 283.4 µs/call mean, 280.0 µs p50
- Size-growing write (small-append): 286.7 µs/call mean, 280.0 µs p50
- Extra cost per call when the write grows the file (append − overwrite): 3.3 µs mean, 0.0 µs p50
- Same 8.2 MB in 125 × 64 KiB calls: 36.7 ms (223 MB/s) vs 573.5 ms for 2,000 × 4 KiB; the small calls are 15.6× slower for the same bytes
- Same 8.2 MB in 8 × 1 MiB calls: 3.5 ms (2341 MB/s) vs 573.5 ms for 2,000 × 4 KiB; the small calls are 163.9× slower for the same bytes
- flush() after every write adds 5662.5 µs/call on top of small-append (append+flush: 5949.2 µs/call mean)
- small-append: first call into the fresh file 520.0 µs vs p50 280.0 / p99 400.0 µs; across runs 320.0–520.0 µs
- chunked-append (64 KiB): first call into the fresh file 440.0 µs vs p50 300.0 / p99 360.0 µs; across runs 280.0–440.0 µs
- chunked-append (1 MiB): first call into the fresh file 420.0 µs vs p50 420.0 / p99 460.0 µs; across runs 380.0–420.0 µs
- append+flush: first call into the fresh file 6960.0 µs vs p50 5960.0 / p99 10540.0 µs; across runs 4960.0–6960.0 µs
- new-file: 113.9 ms per file (9 files/s) = create+open 49.1 + first write 0.6 + close 31.9 + delete 32.3 ms
- new-file without delete (what populating OPFS pays per file): 81.6 ms
- First write into a new file (4 KiB): 586.5 µs vs 286.7 µs per small-append call