# lightning-fs: a small file system layer for the browser

> lightning-fs is a deliberately partial implementation of the Node file system interface that runs in a browser, built on IndexedDB with an in-memory cache on top. The design is defined by a set of exclusions: it is not a complete implementation of the interface, it is not configurable across backends, and it makes a bet that a file system used by a web application is mostly not input and output bound.

**isomorphic-git/lightning-fs** — A lean and fast 'fs' for the browser

- Repository: https://github.com/isomorphic-git/lightning-fs
- Stars: 612 · Forks: 65
- Language: JavaScript
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/isomorphic-git-lightning-fs

## The question it was built to answer

The motivation section is a single sentence, and it is a good one: the author wanted to see whether it was possible to make something faster than the two established browser file system libraries, while still implementing enough of the file system interface to run the isomorphic-git test suite in a browser.

That is a much better framing than a feature list, because it defines success in a way you can check. The test suite of a real project, running in a real browser, is a hard test, and the two libraries named are the ones everybody uses. So the project is not trying to be a general purpose file system for the browser. It is trying to be the fastest thing that passes somebody else's test suite.

The second section sets out what the library is not, and it is blunt about it: it does not even come close to implementing the full file system interface. Instead, it implements only the subset used by the plugin interface of the git client, plus the promise based versions of those same functions.

That is a good trade, and it is the same trade every serious partial implementation makes. The git client's plugin interface is a small, well defined contract, and implementing it exactly is a tractable problem. Implementing the whole interface is not a tractable problem and nobody wants it to be, because most of the interface is about streams and file descriptors that have no meaning in a browser.

The comparison with the other library is equally blunt. That one has a dozen backends and is highly configurable. This one has a single configuration that is supposed to just work for most users.

So the trade is explicit: a much smaller surface, a much narrower set of features, and a much better chance of being fast. The project is MIT licensed, the last push was on 2026-10-17, and the release history shows a steady patch line through the autumn of 2026.

## Four requirements, and what they rule out

The philosophy section lists four basic requirements, and then does the interesting work of showing what each one eliminates.

The first is that it has to work in all modern browsers. The second is that it has to work with reasonably large files and directories. The third is that it has to persist data. And the fourth is that it has to enable fast web applications.

The third requirement is what rules out a pure in-memory solution, because a file system that vanishes when the tab closes is not a file system.

The fourth is what rules out the browser's local storage, and the reason given is specific rather than general: local storage blocks the main thread and cannot be run in a web worker. So an application that does its work in a worker cannot use it, and an application that uses it cannot stay responsive while it writes.

And the first requirement is what rules out the two more capable browser storage interfaces, the older database interface and the browser's own file system interface, because they are not available across all modern browsers.

What is left is the one storage technology that satisfies all four, the browser's built-in object database. So the entire storage design is not a preference; it is what survives the elimination, and the readme says exactly that.

That is a well argued piece of design, and it has a consequence worth stating plainly for anyone considering the library. The storage technology is not a configuration option. There is one backend, and the readme's comparison with the other library makes the point that the other one has a dozen. If your application needs a storage technology that is not the object database, this library is not for you, and no amount of configuration will change that.

The optimisation targets then put the priorities in order, and they are worth noting because they say what the author thinks is scarce. First is speed, the time to execute a file system operation. Second is bundle size, the time to download the library. Third is memory usage, with the question written as whether it will work on a mobile device. The order is a statement about the constraints of the environment, and it is also the order in which the design decisions in the next section follow.

## The hybrid, and why the file contents are not cached

The implementation that comes out of those priorities is a hybrid, and the readme describes it precisely.

The directory level operations, creating a directory, removing one, listing one, renaming, and stat, are pure in-memory operations that take no time at all. The file level operations, writing a file, reading a file, and unlinking one, are throttled by the object database.

And the in-memory part is persisted to the database with a debounce of five hundred milliseconds.

So the fast operations are fast because they never touch persistent storage, and the slow ones are slow because they do. A program that creates a hundred directories and renames them is doing entirely in-memory work, and a program that writes a file is paying for the round trip.

Then there is the decision that is the most interesting one in the readme, and it is a decision not to do something. The file contents are not cached in memory, because the author does not want to waste memory.

The reasoning continues: an application can always add a cache on top of this library, and if the library added one internally and it were not tuned for the application, it would be much harder to work around.

That is a real principle and it is worth naming as one, because caching is exactly the kind of thing that is easy to add and hard to remove. A cache in the library is invisible to the application that has to live with it, and a cache the application adds is a thing the application controls, can size, can invalidate, and can turn off. The same reasoning explains the ordering of the optimisation targets, because a cache costs memory, and memory is the third priority, and the constraint that matters is the one on mobile devices.

So the design is: make the metadata operations free, make the file operations honest, and leave the caching decision to whoever knows the application's access pattern. The debounce is the other half of that, and it is the reason a thousand small writes cost one database transaction rather than a thousand.

## The mutex, and the bet it makes about your application

The multi-threaded section is the most interesting part of the readme, and it starts by stating a problem: multiple tabs and web workers can share a file system, but the in-memory cache that makes the library fast cannot be shared, because the shared array buffer that would allow it is still not available in most browsers.

And then it does something that is either very clever or very risky, depending on your application. If each thread were allowed to update its own cache independently, you would have a distributed system, and you would need a sophisticated algorithm to resolve the conflicts. Instead, the author is counting on the fact that multi-threaded applications are not input and output bound, and a simpler strategy will therefore work.

The strategy is a mutex. File system access is serialised by a lock implemented with polling and an atomic compare and replace operation in the object database, so that only one thread has access at a time. While one thread holds the lock, the others wait. And if the active thread goes idle, meaning no operations are pending and no new ones have started, then after five hundred milliseconds its in-memory cache is serialised and saved, and the lock is released.

The five hundred milliseconds was chosen experimentally, so that a clone operation in the git client would not thrash the lock. That is a very specific piece of evidence: the constant is tuned against the workload the author cares about.

The consequences are then spelled out, and they are worth reading before you use this in a multi-tab application. While the lock is held by another thread, your operations will be stuck waiting. If the lock is not available even after ten minutes, the operations fail with an error. The example given of when that could happen is an application writing to a log file every hundred milliseconds, and the fix given is to make sure the file system is allowed to go idle for more than half a second now and then.

So the design works on the assumption that a multi-threaded application using a file system is doing something else most of the time. For a web application that is mostly idle with occasional bursts of file system work, that assumption holds. For a log writer, it does not, and the readme says so.

## The options, and the fallback to the network

The usage section is short, because the construction is short. You import the class and create a file system with a name, and the name is used to determine the object database store name.

```js
import FS from '@isomorphic-git/lightning-fs';

const fs = new FS("testfs")
```

And then there is a note that is really a warning: it is better not to create multiple instances with the same name in a single thread. Each instance maintains its own cache, so the memory usage is higher, and each has to compete for the lock, so the throughput is lower.

The options table then lists the knobs, and the interesting ones are the ones that change the storage behaviour.

The wipe option deletes the database and starts with an empty file system, which is the one you want in a test. The database and store name options let you customise the names, separately for the file data and for the lock, which is what you need if you want two file systems to coexist.

The defer option avoids lock contention during initialisation, which is a small but well judged addition: the initialisation of a file system has to take the lock, and if several tabs open at once that is exactly when they would fight.

And then the two options that are the most interesting because they break the storage model in a controlled way. The url option lets read requests fall back to an HTTP request to a base address, and the urlauto option extends that to every read of a file that is not backed, even one that should be. So the library can serve a mixture: files that are in the database come from the database, and files that are not come from a web server. That is a hybrid read model, and it is the answer to the case where an application has some large assets it does not want to store in the browser.

There are also two low level options for replacing the storage entirely, a database object to hold the data and a backend interface, and the table notes that if the backend is present then the other arguments have no effect. So the escape hatch is the same in both cases: replace the thing that talks to the browser's storage, and you get a different storage technology with the same library on top.

## The repository, and how it is tested

The repository layout is small, and the interesting files are the test configuration and the build one.

There is a source directory, a type definition file, a webpack configuration, a prettier configuration, a release configuration, a continuous integration directory, a pipeline definition for a second provider, a licence, and the readme. And there is a karma configuration file, which is the one that tells you how the project is tested.

The test command is the karma runner in single run mode, and the development dependencies are a list of launchers: one each for Chrome, Edge, Firefox, Safari, and Internet Explorer, plus a browser stack launcher, a webpack integration, and several reporters, including one that fails fast and one that writes a JUnit report for a continuous integration system. The test framework is Jasmine, and the browser automation is driven by a headless browser tool that is installed as a development dependency.

So the test suite runs in real browsers, and it runs in the ones a library like this has to work in. That is the right choice, and it is the direct consequence of the first requirement: this library exists to make a git client work in a browser, so the tests have to be in browsers, and they have to be in the older ones too, since the whole design argument was about not depending on storage interfaces that are not everywhere.

The presence of a launcher for Internet Explorer is worth a small note. It is a leftover from an era, or a courtesy, but it also tells you the project is not assuming a modern-only audience.

The dependencies are four, and they are all small and specific: a key and value store built on the object database, a text encoding polyfill, a debounce helper, and a helper that runs a function only once. The build is webpack, and the release is handled by a tool that derives the version and the notes from the commit history.

The published files list is interesting: everything that is JavaScript, plus the type definitions, and the test and coverage directories explicitly excluded. And the package manifest names a command line utility alongside the library, a small tool for reading the superblock of a file, which is the kind of thing a project that works with git's storage formats ends up needing.

## Conclusion

lightning-fs fits a browser application that needs a real, persistent file system, most obviously one that runs a git client in the page. It does not fit a project that needs the whole Node interface, because the implementation is a documented subset, and it does not fit a multi-tab application that writes continuously, because the locking model punishes that pattern. Verify first which of the operations you need are implemented, that you can live with the storage technology being chosen for you, since the design rules out the alternatives, and that your application's access pattern is compatible with the lock, because the readme is explicit that a writer that never goes idle will fail after a timeout.

## FAQ

### What is the lightning-fs library?

It is a browser implementation of part of the Node file system interface, built on the browser's object database with an in-memory cache on top. It implements the subset of the interface that the isomorphic-git client needs, plus the promise based versions of those functions, and it is designed to be fast and small rather than complete.

### Why does lightning-fs use IndexedDB rather than another storage technology?

Because the alternatives are each ruled out by one of the project's requirements. A pure in-memory solution cannot persist, the local storage API blocks the main thread and cannot run in a worker, and the older database interface and the browser file system interface are not available across all modern browsers. That leaves one option, and the storage technology is therefore not configurable.

### How does lightning-fs make operations fast?

By splitting the work. The directory level operations are pure in-memory and take no time, and the file level operations are throttled by the database. The in-memory part is persisted with a debounce of five hundred milliseconds, and the file contents are deliberately not cached in memory, because the author expects applications to add a cache of their own if they need one.

### How does lightning-fs handle several tabs using it at once?

With a mutex implemented by polling and an atomic compare and replace in the database, so that only one thread has access at a time. When the active thread goes idle, its cache is saved after five hundred milliseconds and the lock is released. Operations waiting for the lock fail with an error if it is not available within ten minutes, which the readme says can happen to an application that writes to a log file every hundred milliseconds.

### Can lightning-fs fall back to fetching files over HTTP?

Yes. One option lets read requests fall back to an HTTP request to a base address, and another extends that to every read of a file that is not backed, even one that should be. There is also a low level option to replace the storage object or the whole backend, in which case the other arguments have no effect.

## Sources

- [isomorphic-git/lightning-fs on GitHub](https://github.com/isomorphic-git/lightning-fs)
- [Issues](https://github.com/isomorphic-git/lightning-fs/issues)
- [License: MIT](https://github.com/isomorphic-git/lightning-fs/blob/main/LICENSE)
- [README](https://github.com/isomorphic-git/lightning-fs/blob/main/README.md)
- [Releases](https://github.com/isomorphic-git/lightning-fs/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/isomorphic-git-lightning-fs
