Day 183: Two Steps Back
Oof, migrating is such a slog. I’ve found it difficult to do the transition in chunks because everything is affected with state management, so I’ve been making monster changes that feel like all-or-nothing. So glad I’m using Typescript because it makes changing things drastically a lot easier.
Today I talked with Shiva who gave some sage advice about common patterns to adopt and pitfalls to avoid (thanks dawg!!!) and spent the whole day moving (almost) everything over to Redux. Many things are still broken, but I finally got it working enough to be able to run a flow.
The good parts:
- I can see why Redux is so popular. Having a central place where all state changes occur makes it really easy to reason about your application, and it’s dope.
- Redux works well even when you’re not using it with UI components. For example, I’m using Redux to manage state in my background script (because it shares state with other contexts like the popup and content scripts), and it works well even without UI bindings.
- Patterns that seemed fine before now seem silly now that I’m looking at things “the Redux way”. I used to have little bits of state that was shared between a few different files using module exports in my background script, and it’s much nicer to have that stuff be managed in Redux because I can inspect it at any time without having to
console.logall the time.
The bad parts:
-
Even with
createSliceand other helpers fromredux-toolkit, there’s still a ton of boilerplate. I get why all these parts are necessary and the helper functions cut down on a lot of it, but there’s quite a lot that’s required to get the whole system working. It’s especially quite noticeable when using Redux with Typescript. -
This is less a criticism of Redux and more of the usage of Redux in browser extensions, but there’s a major performance hit due to all the message passing required to keep the contexts in sync.
I ran the benchmark test I developed in my previous post and the initial results are… well, let me show you.
Here’s what I had before I incorporated Redux and was using the WebExtensions storage API + throttling to sync state:
(Note: This is much higher than the numbers from the previous post because I learned that recording my screen using QuickTime slows down execution quite a bit. When I run the flow without recording, the flow runs around 3 times faster.)
And here’s what I got after getting Redux working:
Yup. It’s much, much slower.
It’s important to note that the above result has much more to do with the mechanism that’s syncing Redux stores between the popup and the background script rather than Redux itself. The above result is from using
webext-redux— my custom syncing implementation performs slightly better but is pretty much the same as it’s also passing messages to sync up.Yeah, so this isn’t great. Even when I removed the cross-context communication bit out of the equation and compared running the test using just Redux compared with my previous implementation (with saving to storage commented out and everything being done in memory to make it a fairer comparison), there was a substantial difference: ~1800 prints for Redux vs. ~2200 prints for my fat models implementation.
It’s pretty disappointing that I’ll have to optimize performance all over again and that there’s a performance hit to using Redux in Browserflow — I guess that’s part of the tradeoff.
At this point, I’m most likely going to keep Redux given that this just seems like a much saner way to build applications, but I’m a bit miffed that the nature of browser extensions brings some hurdles to using Redux in a performant way.
I’ve also been thinking about whether I should open source the helpers I’ve developed for building extensions. If you’re using vanilla React, I think it’s the best way to work with data from storage as well as share state across different contexts. I’ve spent a lot of time making it quite flexible and performant (there’s a caching layer that’s automatically updated from storage that ensures that you’ll pretty much always get data from memory instantly rather than having to wait to fetch from storage — this is why my implementation above is quite fast). At the same time, packaging it up and writing documentation would take a while and I’m not trying to come up with even more reasons to delay Browserflow. Okay, maybe later.