Introduction

As you know, since 2018 I have been advocating the use of Flutter for all your mobile and even desktop application developments.

From the early versions of Flutter Web in 2019, I wondered when I would be able to use it to create a professional website. It was natural for my website to be written in Flutter Web.

This article is a concrete and neutral feedback where I share the positive points, points that require special attention, and also negative points.

I will conclude with a series of recommendations to help you decide whether or not to use Flutter Web for a website.


Unique Code?

Of course, the first question that naturally comes to mind is "Can I reuse my Flutter code originally designed for my mobile app as is?".

The answer is yes and no. Today, most libraries support the peculiarities of Web vs Desktop vs Mobile quite well, and you can generally use them as they are without worrying about the platform. Therefore, you can easily count on code compatibility close to 95%.

Nevertheless, a Web application is not a mobile application, and therefore, you will need to consider certain browser-specific features and the availability of certain features.

Let's start exploring these differences...


PWA vs Website

The first point I wanted to mention is that Flutter Web does NOT (naturally) generate a website but a Web App!

What's the difference you might ask?

Without considering WebAssembly which is still experimental to date (April 2024)[we will see later], the difference is that when you access a site developed in Flutter Web, your browser must download an entire application before executing it.

Depending on the size and complexity of your web application and the quality of the user's network, this can lead to a significant loading time before anything is displayed. This can lead to a poor image/reputation of your site.

To keep visitors waiting, you can always display a small animation that is launched as soon as the first HTML code is loaded. Still, your visitor will have to wait...

Once the application is launched, you almost no longer need to access your server EXCEPT if you enter a URL directly in the browser bar. At that moment, it comes down to re-downloading the application and re-executing it. Thanks to your browser's cache, this will be faster.


WebAssembly (Wasm)

The introduction of WebAssembly support by Flutter opens the door to new perspectives and improvements.

To summarize, WebAssembly (Wasm) is a standard that allows compiling high-level programming languages, including Dart, to run in a web browser with high performance.

This also allows compiling parts of Dart code, downloading them on demand, and executing them when needed.

One notable improvement is that the web application to be initially downloaded is significantly reduced (thus faster to download). As the needs of the web application evolve, Wasm modules can be dynamically downloaded and executed.

So is this the solution?

Not quite, unfortunately (I remind you that we are in April 2024 as I write these lines...). So why not?

  • Most major browsers (Chromium: Chrome, Opera, Edge) support Wasm, however, Safari does not yet support WasmGC (WebAssembly Garbage Collection)[see the ticket];

  • Many packages use either dart:html or package:js. These packages cannot be compiled to Wasm. This restricts the use of many packages;

  • Especially: currently neither flutter run nor DevTools support Wasm. This makes development and debugging quite complex;

Nevertheless, if you manage to use only compatible packages and the compilation succeeds, it must be admitted that there is no doubt, your application is significantly smaller, loads very quickly, and runs much faster. You can see an example of a site that uses Wasm at https://flutterweb-wasm.web.app

In short... to be continued.

Let's now return to my feedback...


Responsiveness

The first directly visible difference is that your application must support a dynamic layout due to browser resizing.

This can be achieved using basic Widgets, such as Flexible, Wrap, ... but in most cases, it comes down to modifying the application's behavior through source code. The use of specialized packages such as flutter_bootstrap or equivalent that provide grid-based solutions and tend to mimic well-known JavaScript/CSS frameworks is already a great help.

Using MediaQuery, LayoutBuilder, ... also allows you to manipulate the display based on dimensions and/or orientation, much like @media allows in CSS.

My experience is that, just as you must do for a normal website, it takes quite a bit of time for your layout to correctly respond to the variations in dimensions of your browser.

One of the biggest difficulties I encountered was dynamically resizing texts because I wanted to animate and position them precisely, however, as you know, sizing a child Widget (and particularly determining the font size of a text that may or may not wrap according to all constraints ...), in relation to the dimensions of its direct or indirect parent, is not always easy to achieve.

In short, although it takes more time compared to developing a mobile app, it is very doable.

However, as I will return to later, you need to pay much more attention to optimizing your code and limiting the number of builds!


Hot Reload

One of the greatest features of Flutter is the Hot Reload, which allows you to modify your source code and see the change directly without having to restart everything.

Flutter Web does not have a Hot Reload feature (as of April 2024) but supports Hot Restart, which is already not bad but each time, the application restarts.

I used some tricks to save as much time as possible such as making the page I was working on displayed as the "home" and simulating data, while its layout was more or less finished. Nevertheless, there are always modifications to be made afterwards and it is not always possible to use this trick.

Although this may not seem very important, practice says otherwise and the direct consequence is an extension of the development and maintenance time.


Browser Performance on Smartphone vs Desktop

During the initial development, I naturally used my usual browser (Chrome) on my large computer and everything worked impeccably. Everything was smooth. I used the "developers tools" to simulate different models and sizes.

What a surprise when I performed a pre-release and used my iPhone for the first time... The animations were choppy, the scrollings ultra slow, some images did not display... I then used an Android model and it was even worse...

What a disillusion! It was at this moment that I fully realized there was a world of difference in power between my development computer and smartphones and that JavaScript parsing was much slower on Smartphones...

I was then forced to revise a large part of the code and optimize the rebuilds, especially the animations.

The first Widget that was a great help is the little-known RepaintBoundary. This widget allows limiting a repaint to a limited part of the screen, which is ideal for a very localized animation or scrolling.

The second Widget is the Offstage. The advantage of this widget is that it is not repainted if it is not visible (offstage: false). This is particularly interesting when you need to stack Routes, Overlay, ... However, even if it is not displayed, do not forget that the animations it might contain continue to run, using CPU unnecessarily. Therefore, a good practice is also to stop the animations when it is not visible. However, this is not always easy to do if you need to pause an animation that would take place at the level of one of the countless Widgets in the sub-tree... How to warn it and ask it to stop/restart? This requires a planned architecture in advance.

Since this surprise, I have continually used DevTools as well as different models of smartphones to check the repaints and optimize them to the highest point, which should be systematically done in all development cases.

This difference in power between desktops and smartphones, but also between iOS and Android phones, forced me to adapt the contents, animations (complexity and quantity) according to the models... a bit of a shame.


No HTML, Just Graphics

The fact that everything is bitmap and not HTML/CSS is also an important factor and a key difference from developing a traditional website.

It may seem obvious, but how many times when developing a website do we use the Developers Tools of the browser to dynamically modify some CSS properties to check their impact on the display, before changing the SCSS code itself?

As I mentioned earlier, this is entirely feasible through Hot Reload, but in Flutter Web development, Hot Reload does not exist (yet) and the DevTools that we like to use for mobile development unfortunately do not render the same services for the web.


Dependency on HTML Rendering

Following the fact that everything is graphics, some functionalities typically available on a website are much more difficult to perform in Flutter Web.

To illustrate this point, I will take 2 concrete examples.

Case 1. Local href

In some cases, you may need to link certain parts together. For example, if a user clicks on an element of the page (menu, link, ...), you may want to position the browser at the referenced element, as illustrated below.

<a href="#reference">Click here</a>
...
<div id="reference">This is the referenced content</div>

To achieve this in Flutter Web, without using workarounds, you need to know the exact position of the referenced Widget within a Scrollable to force scrolling to that Widget.

Therefore, if your page is very long, you are forced to display everything to obtain the position and thus, you lose the advantage of using Slivers and must set a very large cacheExtent. This increases memory usage.

Case 2. Dynamic rendering of HTML within a Flutter page

Specifically, consider needing to display a Gist in the middle of content purely written in Flutter.

To do this, you would insert HTML code via an HtmlElementView and once inserted in the page, a JavaScript code will make a call to Github to fetch the content and perform the rendering.

Since this rendering is asynchronous and the height of the HTML content is also dynamic, you must find a workaround to adapt your Flutter page accordingly, causing resizes. However, during this whole operation, your page is visible and can give a bad impression to visitors (overlap, ...).

Furthermore, to increase the difficulty, the user may decide to resize their browser, which can lead to a change in the height of your dynamic HTML and thus require resizing the Flutter container, ... In short, it's not always very easy to execute or synchronize.


Memory Leak

My biggest surprise was related to the problem of memory leak concerning images.

Initially, my site used many images displayed in sequence, using animations. Regardless of the method, settings (cacheWidth/cacheHeight, enableMemoryCache, ...) and/or packages used, I consistently had a problem with memory leak regarding images.

When launching the application in the browser, it is not noticeable but after letting the application run for some time, I noticed a decline in the fluidity of animations until I got the infamous "ouch" screen, the browser crashed. This was even more systematic and quicker to reproduce on Firefox.

On Android, it was very easy to see as the fluidity of animations degraded quite quickly.

I ended up having to create my own cache and whenever I needed an image, to point to the unique instance of my cache. To allow the release of resources from unnecessary images, I then use WeakReference and Finalizer. This greatly improved things. However, I could no longer really use the basic Widgets "as is". Too bad.


Another source of memory leak is related to misuse of BuildContext. As a reminder, avoid passing a context as an argument UNLESS you are 100% certain that a Widget to which you are passing the context will NOT outlive the Widget to which the context belongs.


Browser Security & CORS

When developing a web application that requires making calls to other URLs, you must anticipate refusals of type: CORS policy.

During the development phase, the solution is very simple and consists of the following operations on your workstation:

In the folder "%FLUTTER%\bin\cache", delete the file "flutter_tools.stamp".
In the folder "%FLUTTER%\packages\flutter_tools\lib\src\web", edit the file "chrome.dart" as follows:
  1. Locate the line "'--disable-extensions',"
  2. Add the following line directly after: "'--disable-web-security',"

However, once your server is running in production, it gets a bit more complicated, and to solve this problem, I used an nginx server as a proxy but I will come back to this a little later.


SEO

And here is the biggest problem... How to optimize the search engine ranking of a website when it's all bitmap?

There are a few packages that attempt to solve this problem and allow you to dynamically add invisible HTML code to your page.

While this works, it is a practice not to recommend. It's called Cloaking and is considered a fraudulent attempt by most Crawlers.

Why?

Simply because the Crawlers have no way of validating that the added HTML is indeed the actual content, visible to your visitors.

The direct consequence is that often, your "ranking" will be, at best, penalized if not banned by search engines, at worst.


So what can be done?


Nginx (or another) to the Rescue

To solve the CORS and SEO problems, I used Nginx as a web server.

Note: I could have just as well used: Apache, Caddy, Lighttpd, HAProxy, ... but I was more familiar with Nginx.

Solving the CORS Problem

To allow access to a URL pointing to https://gist.github.com, I set up a "reverse proxy" rule as follows:

1location /github-proxy/ {
2  rewrite "^/github-proxy/(.*)" /$1 break;
3  proxy_pass https://gist.github.com;
4  proxy_set_header Access-Control-Allow-Origin *;
5  proxy_set_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
6  proxy_set_header Access-Control-Allow-Headers 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
7  proxy_set_header Access-Control-Expose-Headers 'Content-Length,Content-Range';
8}

Thus, any invocation of a URL type: https://www.flutteris.com/github-proxy/... is relayed to https://gist.github.com/... by adding all the correct headers to the request to avoid being rejected by GitHub.

Solving the SEO Problem

To enable my site to be properly indexed by Google, Bing, ... I had to be more creative.

The solution lies in 4 steps:

  • Step 1: Genere static HTML files.
  • Step 2: Store the HTML files in a static repository on your web server.
  • Step 3: Detect Bots and serve them the static HTML files instead of the application... via the web server.
  • Step 4: Generate a sitemap.xml file and sending it to the Bots.
  • For Step 1, nothing could be simpler...

    A simple generator written in Dart that, for each page of the site, generates an .html file based on a definition file.

    The result is an .html file that does not need to be perfectly formatted (as it will never be displayed to users) but has the following form:

    <!DOCTYPE html>
    <html lang="${language}">
      <head>
        <base href="/">
        <meta charset="UTF-8">
        <meta content="IE=Edge" http-equiv="X-UA-Compatible">
        <title>${seoMetaData.title}</title>
        <meta name="apple-mobile-web-app-capable" content="yes">
        <meta name="apple-mobile-web-app-status-bar-style" content="black">
        <meta name="apple-mobile-web-app-title" content="Flutteris">
        <meta name="author" content="Didier Boelens">
        <meta name="keywords" content="${seoMetaData.keywords}">
        <meta name="description" content="${seoMetaData.description}">
        <meta property="og:title" content="${seoMetaData.title}">
        <meta property="og:type" content="${seoMetaData.type}">
        <meta property="og:url" content="${seoMetaData.url}">
        <meta property="og:site_name" content="${seoMetaData.siteName}">
        <meta property="og:description" content="${seoMetaData.description}">
        <meta property="og:image" content="${seoMetaData.imageUrl}">
        <meta property="twitter:card" content="summary">
        <meta property="twitter:creator" content="${seoMetaData.author}">
        <meta property="twitter:title" content="${seoMetaData.title}">
        <meta property="twitter:description" content="${seoMetaData.description}">
        <meta property="twitter:image" content="${seoMetaData.imageUrl}">
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
        <style>
          ${cssContent}
        </style>
      </head>
      <body>
        <h1>${seoData.h1}</h1>
        ${seoData.html}
      </body>
    </html>

  • Step 2 is the simplest of all. You transfer all your generated HTML files to your web server in a folder. For the purposes of this explanation, let's call this folder: /snapshot


  • Step 3 involves setting up redirection rules for Bots & Crawlers.

    To illustrate this, the first thing to do is determine whether the request comes from a real user or a Bot/Crawler. If the request is from a Crawler, then redirect the request to the corresponding page stored as a static resource in the /snapshot folder.

    The solution I found is as follows (based on Nginx):

    
    server {
      listen 443 ssl;
      listen [::]:443 ssl;
      server_name www.flutteris.com _;
    
      #
      # Check if request has been issued by a Bot, Crawler (SEO indexing)
      #
      set $is_bot 0;
      if ($http_user_agent ~* "Googlebot|Bingbot|Yandex|Baidu|WhatsApp|DiscordBot|facebookexternalhit|Twitterbot|LinkedInBot|Slackbot|TelegramBot|redditbot|coccocbot") {
          set $is_bot 1;
      }
    
      location / {
          set $root_path /xxxxxxxxxxxxxxxxxxxx/_data;
    
    
          if ($is_bot = 1) {
              set $root_path /xxxxxxxxxxxxxxxxxx/www;
    
              # Remove the trailing slash from the URLs
              rewrite ^/(.*)/$ /$1 permanent;
    
              # For URL without any languages
              rewrite "^/$" /snapshot/en/index.html break;
              rewrite "^/blog/?$" /snapshot/blog/en/index.html break;
              rewrite "^/flutter/?$" /snapshot/flutter/en/index.html break;
              rewrite "^/privacy/?$" /snapshot/privacy/en/index.html break;
              rewrite "^/terms/?$" /snapshot/terms/en/index.html break;
    
    
              # For URL without languages
              rewrite "^(/(fr|en)?)?$" /snapshot/$1/index.html break;
              rewrite "^/blog(/(fr|en)?)?$" /snapshot/blog/$1/index.html break;
              rewrite "^/flutter(/(fr|en)?)?$" /snapshot/flutter/$1/index.html break;
              rewrite "^/privacy(/(fr|en)?)?$" /snapshot/privacy/$1/index.html break;
              rewrite "^/terms(/(fr|en)?)?$" /snapshot/terms/$1/index.html break;
    
              # For the URL /blog
              rewrite "^/blog/([^/]+)$" /snapshot/blog/en/$1.html break;
              rewrite "^/blog/(fr|en)/([^/]+)$" /snapshot/blog/$1/$2.html break;
    
              # For the URL /flutter
              rewrite "^/flutter/([^/]+)$" /snapshot/flutter/en/$1.html break;
              rewrite "^/flutter/(fr|en)/([^/]+)$" /snapshot/flutter/$1/$2.html break;
          }
    
          # If this is not a Bot, serve the normal Flutter content
          root $root_path;
          try_files $uri $uri/ /index.html;
      }
      ...
    

  • For Step 4, also using a generator, a sitemap.xml file is composed, which will have the following form:

    <?xml version="1.0" encoding="UTF-8"?>
    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
      <url>
        <loc>https://www.flutteris.com/fr</loc>
        <lastmod>2023-10-30</lastmod>
      </url>
      <url>
        <loc>https://www.flutteris.com/blog/fr</loc>
        <lastmod>2023-10-30</lastmod>
      </url>
      <url>
        <loc>https://www.flutteris.com/flutter/fr</loc>
        <lastmod>2023-10-30</lastmod>
      </url>
      <url>
        <loc>https://www.flutteris.com/terms/fr</loc>
        <lastmod>2023-10-30</lastmod>
      </url>
      <url>
        <loc>https://www.flutteris.com</loc>
        <lastmod>2023-10-30</lastmod>
      </url>
      ...
    </urlset>

    Now, all we have to do is provide this sitemap.xml file so that the site is correctly indexed.


    Even though it works, it is still a bit of a hack...


Why is my site no longer in Flutter?

I suppose you have noticed that my site is no longer in Flutter but that I have reverted to a technology more suited for websites.

The main reasons are as follows:

  1. Loading time was too slow.

    As I previously mentioned, this could most certainly be resolved with Wasm, but it is not yet quite ready nor compatible with all browsers.


  2. I received some user feedback saying they experienced crashes on certain browser versions.

    I have never personally encountered this issue, but I assume it could stem from complexity in animations, transitions, ... and memory.


  3. Displaying hybrid contents (HTML + Flutter).

    As I mentioned earlier, dynamically adapting the heights of Flutter containers based on the actual dynamic dimensions of HTML content caused display issues and did not give a very professional impression.


  4. Unnatural SEO indexing.

    Although the workaround I implemented worked, it was only a temporary solution.



Does this mean that Flutter Web is not a solution for a professional public website?

If you refer to the official site of Flutter, you will read that Flutter recommends using HTML for static sites, just as Flutter's own site is written conventionally.


Conclusion

I remain convinced that to date, Flutter is the best framework that allows writing cross-platform applications.

With very little variation in the code, you can get the same application that runs on 99% of smartphones and tablets, desktops (Microsoft, Mac, Linux) and even on Raspberry PI, it works very well.

The advantage is real.

In terms of the notion of web, it is necessary to differentiate between a WebApp (PWA) and a Website.

If you need a web application where the notion of SEO and loading speed are not critical, then Flutter remains a solution to seriously consider.

However, if you want to generate a professional website that will be your showcase on the Internet, as mentioned by the Flutter team, I would not recommend Flutter but rather a conventional solution.

If you are now embarking on designing a PWA, the performance difference between browsers running on smartphones and desktops is a factor to be highly considered. This could lead to reevaluating the ambitions of your application in terms of complexity and/or animations.


However, I am eagerly awaiting more mature support for WebAssembly...


I hope this feedback has enlightened you somewhat and answered some questions.


Stay tuned for future articles and until then, happy coding!