OPFS sync write benchmark

2026-09-17T07:07:26.121Z · Apple M4 Pro (12 threads), 26 GB, darwin 25.6.0 arm64

Config: preset=full, N=20000, S=4096 B, chunks=64 KiB / 1 MiB, R=5, M=2000 · Profiles on: apfs (/)

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 153Firefox 155
1 µs10 µs100 µs1 mssmall-appendChrome 153 — small-append: mean 5.3 µs/call, p50 5.0 µs, p99 10.0 µs, total 105.0 msChrome 153 — small-append: mean 5.3 µs/call, p50 5.0 µs, p99 10.0 µs, total 105.0 msFirefox 155 — small-append: mean 34.0 µs/call, p50 40.0 µs, p99 40.0 µs, total 680.8 msFirefox 155 — small-append: mean 34.0 µs/call, p50 40.0 µs, p99 40.0 µs, total 680.8 ms6.5×small-overwriteChrome 153 — small-overwrite: mean 2.3 µs/call, p50 0.0 µs, p99 10.0 µs, total 46.6 msChrome 153 — small-overwrite: mean 2.3 µs/call, p50 0.0 µs, p99 10.0 µs, total 46.6 msFirefox 155 — small-overwrite: mean 30.2 µs/call, p50 20.0 µs, p99 40.0 µs, total 604.7 msFirefox 155 — small-overwrite: mean 30.2 µs/call, p50 20.0 µs, p99 40.0 µs, total 604.7 ms13.0×chunked-append (64 KiB)Chrome 153 — chunked-append (64 KiB): mean 10.1 µs/call, p50 10.0 µs, p99 50.0 µs, total 12.7 msChrome 153 — chunked-append (64 KiB): mean 10.1 µs/call, p50 10.0 µs, p99 50.0 µs, total 12.7 msFirefox 155 — chunked-append (64 KiB): mean 30.5 µs/call, p50 20.0 µs, p99 60.0 µs, total 38.1 msFirefox 155 — chunked-append (64 KiB): mean 30.5 µs/call, p50 20.0 µs, p99 60.0 µs, total 38.1 ms3.0×chunked-append (1 MiB)Chrome 153 — chunked-append (1 MiB): mean 95.9 µs/call, p50 65.0 µs, p99 350 µs, total 7.6 msChrome 153 — chunked-append (1 MiB): mean 95.9 µs/call, p50 65.0 µs, p99 350 µs, total 7.6 msFirefox 155 — chunked-append (1 MiB): mean 96.7 µs/call, p50 80.0 µs, p99 260 µs, total 7.6 msFirefox 155 — chunked-append (1 MiB): mean 96.7 µs/call, p50 80.0 µs, p99 260 µs, total 7.6 ms1.0×append+flushChrome 153 — append+flush: mean 205 µs/call, p50 190 µs, p99 335 µs, total 4.1 sChrome 153 — append+flush: mean 205 µs/call, p50 190 µs, p99 335 µs, total 4.1 sFirefox 155 — append+flush: mean 62.9 µs/call, p50 60.0 µs, p99 100 µs, total 1.3 sFirefox 155 — append+flush: mean 62.9 µs/call, p50 60.0 µs, p99 100 µs, total 1.3 s0.3×Firefox 155 ÷ Chrome 153
WorkloadChrome 153 µs/callp50 / p99 (µs)totalFirefox 155 µs/callp50 / p99 (µs)totalFirefox 155 ÷ Chrome 153
small-append
20000 × write(4096 B) at offset = current size; the file grows on every call
5.3 µs5 / 10105.0 ms34.0 µs40 / 40680.8 ms6.5×
small-overwrite
20000 × write(4096 B) at offset 0 into a file pre-sized to 4096 B; the size never changes
2.3 µs0 / 1046.6 ms30.2 µs20 / 40604.7 ms13.0×
chunked-append (64 KiB)
1250 × write(65536 B) appending; the same 81920000 B total as small-append
10.1 µs10 / 5012.7 ms30.5 µs20 / 6038.1 ms3.0×
chunked-append (1 MiB)
79 × write(1048576 B) appending; the same 81920000 B total as small-append
95.9 µs65 / 3507.6 ms96.7 µs80 / 2607.6 ms1.0×
append+flush
small-append with flush() after every write (20000 × 4096 B)
205 µs190 / 3354.1 s62.9 µs60 / 1001.3 s0.3×

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 153Firefox 155
10 µs100 µs1 ms10 msnew-file: create+openChrome 153 — new-file: create+open: mean 262 µs/call, p50 240 µs, p99 360 µs, total 525.0 msChrome 153 — new-file: create+open: mean 262 µs/call, p50 240 µs, p99 360 µs, total 525.0 msFirefox 155 — new-file: create+open: mean 931 µs/call, p50 880 µs, p99 1180 µs, total 1.9 sFirefox 155 — new-file: create+open: mean 931 µs/call, p50 880 µs, p99 1180 µs, total 1.9 s3.5×new-file: first writeChrome 153 — new-file: first write: mean 59.4 µs/call, p50 50.0 µs, p99 80.0 µs, total 118.8 msChrome 153 — new-file: first write: mean 59.4 µs/call, p50 50.0 µs, p99 80.0 µs, total 118.8 msFirefox 155 — new-file: first write: mean 39.9 µs/call, p50 40.0 µs, p99 80.0 µs, total 79.9 msFirefox 155 — new-file: first write: mean 39.9 µs/call, p50 40.0 µs, p99 80.0 µs, total 79.9 ms0.7×new-file: closeChrome 153 — new-file: close: mean 82.1 µs/call, p50 80.0 µs, p99 145 µs, total 164.2 msChrome 153 — new-file: close: mean 82.1 µs/call, p50 80.0 µs, p99 145 µs, total 164.2 msFirefox 155 — new-file: close: mean 466 µs/call, p50 440 µs, p99 600 µs, total 932.3 msFirefox 155 — new-file: close: mean 466 µs/call, p50 440 µs, p99 600 µs, total 932.3 ms5.7×new-file: deleteChrome 153 — new-file: delete: mean 112 µs/call, p50 100.0 µs, p99 180 µs, total 223.7 msChrome 153 — new-file: delete: mean 112 µs/call, p50 100.0 µs, p99 180 µs, total 223.7 msFirefox 155 — new-file: delete: mean 708 µs/call, p50 660 µs, p99 860 µs, total 1.4 sFirefox 155 — new-file: delete: mean 708 µs/call, p50 660 µs, p99 860 µs, total 1.4 s6.3×new-file: full cycleChrome 153 — new-file: full cycle: mean 514 µs/call, p50 455 µs, p99 680 µs, total 1.0 sChrome 153 — new-file: full cycle: mean 514 µs/call, p50 455 µs, p99 680 µs, total 1.0 sFirefox 155 — new-file: full cycle: mean 2135 µs/call, p50 2040 µs, p99 2640 µs, total 4.3 sFirefox 155 — new-file: full cycle: mean 2135 µs/call, p50 2040 µs, p99 2640 µs, total 4.3 s4.2×Firefox 155 ÷ Chrome 153
WorkloadChrome 153 µs/callp50 / p99 (µs)totalFirefox 155 µs/callp50 / p99 (µs)totalFirefox 155 ÷ Chrome 153
new-file: create+open
getFileHandle({create: true}) + createSyncAccessHandle() on a name that does not exist yet
262 µs240 / 360525.0 ms931 µs880 / 11801.9 s3.5×
new-file: first write
first write(4096 B, {at: 0}) into the empty file
59.4 µs50 / 80118.8 ms39.9 µs40 / 8079.9 ms0.7×
new-file: close
close() on the access handle
82.1 µs80 / 145164.2 ms466 µs440 / 600932.3 ms5.7×
new-file: delete
removeEntry(name)
112 µs100 / 180223.7 ms708 µs660 / 8601.4 s6.3×
new-file: full cycle
create+open → write 4096 B → close → delete, per file
514 µs455 / 6801.0 s2135 µs2040 / 26404.3 s4.2×

Environment

BrowserVersionModeProfile onperformance.now() stepIsolatedExecutableUser agent
Chrome 153Chrome/153.0.8010.36headlessapfs (/)5 µsyes/Users/theo/.cache/puppeteer/chrome/mac_arm-153.0.8010.36/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for TestingMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.0.0 Safari/537.36
Firefox 155firefox/155.0.1headlessapfs (/)20 µsyes/Users/theo/.cache/puppeteer/firefox/mac_arm-stable_155.0.1/Firefox.app/Contents/MacOS/firefoxMozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:155.0) Gecko/20100101 Firefox/155.0
Chrome 153: Bugzilla summary
**OPFS `FileSystemSyncAccessHandle.write()` per-call overhead benchmark**

Environment:
- User agent: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.0.0 Safari/537.36`
- Platform: MacIntel; hardwareConcurrency: 12
- Timestamp: 2026-09-17T07:07:26.121Z
- 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 | 105.0 | 103.0–105.7 | 190,440 | 780.0 | 5.0 | 10.0 | 10.0 | 735.0 | 85.0 |
| small-overwrite | 20,000 | 4,096 | 46.6 | 45.7–47.0 | 429,185 | 1757.9 | 0.0 | 5.0 | 10.0 | 20.0 | 5.0 |
| chunked-append (64 KiB) | 1,250 | 65,536 | 12.7 | 12.3–12.8 | 98,542 | 6458.0 | 10.0 | 15.0 | 50.0 | 215.0 | 60.0 |
| chunked-append (1 MiB) | 79 | 1,048,576 | 7.6 | 7.5–15.8 | 10,422 | 10807.4 | 65.0 | 285.0 | 350.0 | 350.0 | 195.0 |
| append+flush | 20,000 | 4,096 | 4090.8 | 4000.4–6629.7 | 4,889 | 20.0 | 190.0 | 290.0 | 335.0 | 7675.0 | 595.0 |
| new-file: create+open | 2,000 | – | 525.0 | 490.1–571.3 | 3,810 | – | 240.0 | 290.0 | 360.0 | 20285.0 | 410.0 |
| new-file: first write | 2,000 | 4,096 | 118.8 | 102.1–142.5 | 16,829 | 68.9 | 50.0 | 60.0 | 80.0 | 12580.0 | 70.0 |
| new-file: close | 2,000 | – | 164.2 | 143.4–195.2 | 12,183 | – | 80.0 | 95.0 | 145.0 | 255.0 | 90.0 |
| new-file: delete | 2,000 | – | 223.7 | 202.2–249.0 | 8,943 | – | 100.0 | 110.0 | 180.0 | 17650.0 | 115.0 |
| new-file: full cycle | 2,000 | 4,096 | 1028.5 | 938.2–1112.0 | 1,945 | 8.0 | 455.0 | 520.0 | 680.0 | 57520.0 | 670.0 |

Derived:
- Fixed per-call floor (small-overwrite, size never changes): 2.3 µs/call mean, 0.0 µs p50
- Size-growing write (small-append): 5.3 µs/call mean, 5.0 µs p50
- Extra cost per call when the write grows the file (append − overwrite): 2.9 µs mean, 5.0 µs p50
- Same 82 MB in 1,250 × 64 KiB calls: 12.7 ms (6458 MB/s) vs 105.0 ms for 20,000 × 4 KiB; the small calls are 8.3× slower for the same bytes
- Same 82 MB in 79 × 1 MiB calls: 7.6 ms (10807 MB/s) vs 105.0 ms for 20,000 × 4 KiB; the small calls are 13.9× slower for the same bytes
- flush() after every write adds 199.3 µs/call on top of small-append (append+flush: 204.5 µs/call mean)
- small-append: first call into the fresh file 85.0 µs vs p50 5.0 / p99 10.0 µs; across runs 60.0–135.0 µs (far above p99)
- chunked-append (64 KiB): first call into the fresh file 60.0 µs vs p50 10.0 / p99 50.0 µs; across runs 55.0–100.0 µs
- chunked-append (1 MiB): first call into the fresh file 195.0 µs vs p50 65.0 / p99 350.0 µs; across runs 120.0–195.0 µs
- append+flush: first call into the fresh file 595.0 µs vs p50 190.0 / p99 335.0 µs; across runs 595.0–775.0 µs
- new-file: 0.5 ms per file (1,945 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.4 ms
- First write into a new file (4 KiB): 59.4 µs vs 5.3 µs per small-append call
Firefox 155: Bugzilla summary
**OPFS `FileSystemSyncAccessHandle.write()` per-call overhead benchmark**

Environment:
- User agent: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:155.0) Gecko/20100101 Firefox/155.0`
- Platform: MacIntel; hardwareConcurrency: 12
- Timestamp: 2026-09-17T07:08:04.832Z
- performance.now() resolution in the worker: 20 µs (crossOriginIsolated: true)
- navigator.storage.estimate(): 0.5 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 | 680.8 | 665.0–682.7 | 29,376 | 120.3 | 40.0 | 40.0 | 40.0 | 4520.0 | 40.0 |
| small-overwrite | 20,000 | 4,096 | 604.7 | 597.7–620.4 | 33,073 | 135.5 | 20.0 | 40.0 | 40.0 | 160.0 | 20.0 |
| chunked-append (64 KiB) | 1,250 | 65,536 | 38.1 | 37.6–41.6 | 32,774 | 2147.9 | 20.0 | 40.0 | 60.0 | 180.0 | 40.0 |
| chunked-append (1 MiB) | 79 | 1,048,576 | 7.6 | 7.6–7.8 | 10,340 | 10722.5 | 80.0 | 200.0 | 260.0 | 260.0 | 100.0 |
| append+flush | 20,000 | 4,096 | 1258.8 | 1206.5–1397.1 | 15,888 | 65.1 | 60.0 | 80.0 | 100.0 | 5060.0 | 140.0 |
| new-file: create+open | 2,000 | – | 1862.2 | 1798.3–4966.7 | 1,074 | – | 880.0 | 1000.0 | 1180.0 | 24020.0 | 1100.0 |
| new-file: first write | 2,000 | 4,096 | 79.9 | 78.7–85.0 | 25,038 | 102.6 | 40.0 | 40.0 | 80.0 | 220.0 | 60.0 |
| new-file: close | 2,000 | – | 932.3 | 923.5–4260.5 | 2,145 | – | 440.0 | 500.0 | 600.0 | 28440.0 | 420.0 |
| new-file: delete | 2,000 | – | 1416.6 | 1393.5–3855.7 | 1,412 | – | 660.0 | 760.0 | 860.0 | 29580.0 | 720.0 |
| new-file: full cycle | 2,000 | 4,096 | 4270.7 | 4227.5–13170.1 | 468 | 1.9 | 2040.0 | 2280.0 | 2640.0 | 25540.0 | 2540.0 |

Derived:
- Fixed per-call floor (small-overwrite, size never changes): 30.2 µs/call mean, 20.0 µs p50
- Size-growing write (small-append): 34.0 µs/call mean, 40.0 µs p50
- Extra cost per call when the write grows the file (append − overwrite): 3.8 µs mean, 20.0 µs p50
- Same 82 MB in 1,250 × 64 KiB calls: 38.1 ms (2148 MB/s) vs 680.8 ms for 20,000 × 4 KiB; the small calls are 17.9× slower for the same bytes
- Same 82 MB in 79 × 1 MiB calls: 7.6 ms (10723 MB/s) vs 680.8 ms for 20,000 × 4 KiB; the small calls are 89.1× slower for the same bytes
- flush() after every write adds 28.9 µs/call on top of small-append (append+flush: 62.9 µs/call mean)
- small-append: first call into the fresh file 40.0 µs vs p50 40.0 / p99 40.0 µs; across runs 40.0–60.0 µs
- chunked-append (64 KiB): first call into the fresh file 40.0 µs vs p50 20.0 / p99 60.0 µs; across runs 40.0–60.0 µs
- chunked-append (1 MiB): first call into the fresh file 100.0 µs vs p50 80.0 / p99 260.0 µs; across runs 100.0–160.0 µs
- append+flush: first call into the fresh file 140.0 µs vs p50 60.0 / p99 100.0 µs; across runs 100.0–140.0 µs
- new-file: 2.1 ms per file (468 files/s) = create+open 0.9 + first write 0.0 + close 0.5 + delete 0.7 ms
- new-file without delete (what populating OPFS pays per file): 1.4 ms
- First write into a new file (4 KiB): 39.9 µs vs 34.0 µs per small-append call