📉 Why We Had To Leave Flutter Web
I share the challenges we faced at a past role with Flutter's cross-platform development for web and mobile.
I was speaking to a colleague yesterday about cross-platform development. They are building a product targeting mobile and web from Flutter. At Memo, we did this, and we experienced enormous challenges trying to maintain the web experience from one code base. For us, it came down to 4 things:
Flutter render methodology: The way Flutter renders content on the web, using WebGL and canvas-based techniques, was not well-suited for our use case. As a content-heavy, collaborative platform, we needed an approach that would provide better text handling experience than Flutter's WebGL-based rendering.
Divergent User Experiences: The user experience requirements for our mobile and web applications were quite different. Tasks like long-form content creation were simply more suited to a desktop environment, while mobile was better for quick interactions and consumption. Trying to shoehorn a single UI/UX into both platforms proved challenging.
Limited Library Support: We found that some key libraries and tools, such as parts of Firebase and Google Analytics, had limited or no support for Flutter Web. This forced us to adopt workarounds or find alternative solutions, adding complexity and ultimately time to our roadmap.
Mismatched Unit Testing: Flutter by default runs unit tests in the Dart VM. However, the behavior of certain features, like our CRDT-based synchronization logic, differed significantly between Dart and the actual JavaScript runtime in browsers. This made it difficult to trust our test suite and ensure consistent behavior across platforms.
Flutter Web at Memo
In this post, I’d like to share the challenges we faced so you can confidently evaluate Flutter for your application and truly leverage the cross-platform capability. I believe when Flutter fits, its a wonderful, ergonomic platform. However when it doesn’t you are destining yourself, as we did, to rewrite your application in a web first technology such Svelte.
Memo was a social network for entrepreneurs to connect with others and build their personal and professional brand. Memo facilitated collaborative content creation elements we called memos. Memos could promote expertise, interesting products, interesting people or services, or whatever. Ergo, Memo was a content intensive application targeting both mobile and web. In hindsight, this is nearly the worst use case scenarios for Flutter Web. However, the subtly of this line between web site and web application within our own product was not clear when we started. For the case of Memo, the relevant delineation is web application vs web site. Dane Mackier posted an excellent comparison on twitter last year1.
“...web applications have complex user interactions to achieve a task. Web sites conversely are intended to disseminate information or content.”
What we failed to realize at Memo was that we needed a Web application to create content for a website. Our product was not one target. Instead, we had a two-sided marketplace problem. One side was the brand builders: they want to create content in interesting, collaborative, creative ways. The other side, however was an entirely different target. The brand builders, built content for audiences, audiences who read, and consume it. I now realize that considering the audience’s motives would have made this more apparent. This is especially important in two-sided markets where your audiences are unique. This brings us to canvas rendering, the first challenge and in my opinion the most critical decision point if you are trying to evaluate Flutter Web as a solution.
Canvas Rendering
Flutter (as of this writing in 2024) has two renders: canvaskit, and skwasm2. When we started with Flutter Web, canvaskit was the only offering, however neither alleviate the challenge we faced at Memo. The beautiful aspect of Flutter’s approach here is that Flutter gives you access from Dart to WebAssembly. If your product demands the benefits and can tolerate the tradeoffs of WebAssembly, Flutter/Dart is a wonderful onramp to that world. Yet, web assembly, as implemented by Flutter causes a high time-to-first-interact. The WebAssembly has a greater than 1MB dependency before the app starts to render. While this is cached for frequent users of your app, if you are trying to serve pages quickly to net new or infrequent visitors your site will load slowly as the WebAssembly dependency downloads. If you are serving content to mobile devices over cellular, this high latency will quickly earn your app the reputation of being slow.
For Memo, a content focused B2C application, this phrase “...the UI is rendered on the main thread into WebGL…” sunk us. Our app didn’t fit as a WebGL target. It wasn’t a game, it wasn’t a graphics intensive platform like Figma, we were serving text content. “rendered into WebGL” includes the text too. Because fonts are rendered via graphics primatives, the text “isn’t actually there”. While Flutter’s native widgets are able to elide this and make the text selectable, if you use custom content rendering to get richer formatting than the native components allow, your text will not be highlightable, or selectable.
Effectively, Flutter is drawing the text onto an image, and serving you that image. This is fine in a game. Game content, one doesn’t expect to highlight or copy text. However on the web this is an expected idiom, one which Flutter doesn’t meet. Your website’s content is also opaque to search engines. If your content needs to be discoverable outside of your app’s ecosystem, you’re now looking at server side rendering adding additional complexity. This is a difference between the ecosystem a web application typically lives within vs a mobile one and this difference is an unfortunate theme of Flutter Web. The web and mobile are different platforms, the screen size difference alone effects the kinds of interactions that are expected. In the next section I will compare how the one code base, cross-platform promise may be a lie for your application.
Cross-Platform User Experience (UX)
I define User Experience (UX) as a set of expectations and learned behaviors users have for accomplishing tasks in an application.
When your app meets expectations, your app is called “intuitive”. When your app fails to meet expectations, your app is called “frustrating”.
Imagine an example we faced at Memo: content creation. You are writing a long form newsletter. You want rich formatting options, headings, links and media. Would you do this on your phone? Probably not. Instead, you’d sit down and write the content with a full keyboard. Thus, you want a different creation experience on mobile, and more critically, you create different kinds of content on mobile versus on desktop. The UX between these two modalities is simply a different set of non-overlapping expectations.
Layout
On mobile you want an experience optimized for navigating with your thumb. Typically your right thumb. On desktop, your eye tracks (for western audiences) from the left to the right. Thus, on the web important elements tend to be at the top left, whereas mobile, the most important elements tend to be bottom right. When designing your layout in order to capture these differences, you’ll likely have a conditional that loads the web version of the layout vs the mobile version of the layout. Yes, technically it’s one code base, but really you have two independent paths each of which have to be tested as independent platforms. Hence, you don’t get the benefit of testing one layout and having any confidence the other platform works as well. Between iOS and Android you largely get test coverage testing from one or the other platform, but for mobile to web, you do not. It’s still tested separately.
Responsive
Responsive is the dynamic collapsing and changing of UI elements as the screen size changes. On mobile, responsive is primarily a few fixed positions3 of which Flutter helps you by providing reasonable scaling tools. Web however, the whole page can be dynamically scaled. One must carefully design the websites breakpoints to assure the app remains usable across a range of window sizes. Mobile isn’t primarily focused on breakpoints, hence each platform has to be considered independently. Compare this to building a game that targets web and mobile. In a game responsive is not really a consideration. The game space is fixed, and you don’t expect the user to dynamically scale the screen to hide and appear elements. Yet again it depends on use case. Flutter can be a great tool in the browser, when its limitations are assets to your application.
Library Support
In my last post we discussed choosing a language. In that post I assert that one should consider the library and ecosystem of the language before you considering the merits of the language itself. My experience at Memo is partially what colored that opinion. When we were using Flutter web, web was a second-class citizen in the Flutter ecosystem. Some sections of Firebase didn’t support Flutter web. Some sections of Google Analytics didn’t support Flutter web. The developer experience tools like monitoring had limited Flutter support. This is a constantly changing situation as the ecosystem continues to mature and highly dependent on what you are building, but my caution here is “just because the library says it supports flutter, does not mean it supports flutter web.”
Unit Tests
Unit Tests depend highly on the type of app you are building. Coming from airplanes and machine control I used to have a much stricter attitude toward unit test everything! However, I now see how the success of an app may be more depending on time to market, and in those instance there may be space for a move fast and break things startup. In that situation, this may be a minor issue. However as your application size increases and the complexity of your app increases, especially after product-market-fit, you’ll want unit tests before you drown in a mire of manual testing across multiple platforms and responsive screen sizes.
By default your unit tests run in the dart VM. This is great and fast4, however some behaviors are remarkably different between the browser and the dart vm. Thus your unit tests passing in CI are representative for mobile, but can be a complete lie in the browser. This is “a feature” for the dart team5.
However, if you are unaware of this discrepancy, your code can behave quite strangely and you’ll be unable to trust your unit tests. At Memo, we had a Conflict-Free-Replicated-Datatype (CRDT) backed feature that synchronized content between your client and the server. Each element used a hybrid-logical-clock (HLC) to deterministically order distributed events.
This worked great on mobile. However despite exceptional test coverage, in web we’d get events jumping inconsistently in time. The reason is because JavaScript doesn’t have integers. In JavaScript, everything is a double. You don’t have a precise number type. To get one you have to depend on a library6. The problem here is that the behavior of dart is depending on the ecosystem. Flutter does let you run your unit tests in headless browsers7 to give you additional confidence. They run slower, but at least this is an option, however it’s just yet one more area where you have to consider web separately from the rest of your application.
Conclusion
Flutter is an incredible ecosystem with many strengths, yet the web remains a distinct platform with its own unique considerations. Dart is an expressive, wonderful language. It’s easy to learn. I upskilled a team of 3 to be productive in Dart in a few weeks, and to a professional level in a few months. Kotlin, Swift also are great for this kind of productivity, but via Dart you can target both major mobile platforms from one code base. That one code base argument however starts to deteriorate, or even outright lie when you add Web to the mix. The idioms of web, the differences between the dart vm and the JavaScript, the high time-to-first-interact and WebGL rendering of everything, can be non-starters for some apps.
My advice would be to carefully evaluate your specific requirements and use cases before committing to Flutter for both mobile and web development. Otherwise you may end up claiming “one code base” when instead it is littered with diverging
if(web) //Now for something completely different
In our case, we ultimately decided to rewrite the web portion of Memo using a more web-centric technology to better align with our user expectations.
Dane Mackier [@DaneMackier], “As a Flutter expert I want to say, don’t use Flutter web for websites. It’s not meant for it. Instead, use it for web applications. Here’s the difference 🧵,” Twitter. Accessed: Nov. 05, 2024. [Online]. Available: https://x.com/DaneMackier/status/1678750725566783488 ↩
Flutter Development Team, “Web renderers,” Flutter Docs. Accessed: Nov. 05, 2024. [Online]. Available: https://docs.flutter.dev/platform-integration/web/renderers ↩
Especially if you are not targeting tablets. ↩
Fast gets thrown around a lot. In this case, I mean running a unit test in the DartVM is faster than starting a headless chrome browser, loading the transpiled code into it, running the test, and evaluating the result. Faster than that. ↩
Dominik Roszkowski [@OrestesGaolin], “Did you know that #Flutter uses JavaScript’s inability to handle integers to determine if it’s running in web? https://t.co/NG7fsbDfN7,” Twitter. Accessed: Nov. 05, 2024. [Online]. Available: https://x.com/OrestesGaolin/status/1250134458478583809 ↩
We chose the fixnum library and it resolved the problem quickly. ↩
Lxxyx, “flutter/docs/contributing/testing/Running-Flutter-Driver-tests-with-Web.md at master · flutter/flutter,” GitHub Flutter. Accessed: Nov. 05, 2024. [Online]. Available: https://github.com/flutter/flutter/blob/master/docs/contributing/testing/Running-Flutter-Driver-tests-with-Web.md ↩