There are (more or less) three ways to build a cross-platform app.
- Use the standard UI widget system on each platform, and then try to provide a higher level abstraction. This is the strategy used by Java's AWT and (I believe) React Native.
- Use a custom canvas. nowadays this might be Skia, eg. that's what Flutter and JetBrains Compose use. This is the strategy used by Java's Swing and JavaFX as well. This is also how Unity, Godot, and other game engines work.
- Use an HTML rendering engine, either via by embedding the engine (e.g. Electron) or by using WebView (loading the default OS provided engine, e.g. Tauri).
I've been doing this a terribly long time and have built stuff with all of the stacks mentioned above except React Native.
Part of the challenge is that the trivial version (just dropping a few pixels on screen) is comparatively easy. Once you start adding in things like font rendering, support for complex layouts, support for assistive devices, support for full internationalization (e.g. different language systems), etc etc etc. it gets very complicated. This is especially true once you want to start offering customized controls.
The type 1 above has, in my experience, historically proven to be pretty easy for the trivial app but extraordinarily difficult to deal with once the app becomes non-trivial. Most notably, pretty much all of the custom x-plat controls wind up just opening up a canvas and punching bits in. This can absolutely hash perf and blow up the bug count.
The type 2 is what usually happens to a type 1 system that hits the wall, or when someone with experience building a type 1 decides to build a new framework. This works if the app just throws out any pretense of a standard UI (e.g. a game engine) but usually throws out lots of functionality (esp assistive devices).
Both type 1 and type 2 frameworks tend to struggle, in particular because they start to be a LOT of work (money) to maintain, but they really struggle to get traction, in part because there's really no good business model. Developers have been trained over the decades to expect free SDKs for building apps. There have been a few examples of this kind of thing (e.g. Flash) over the years, but HTML has just grown and grown.
Which brings us to the type 3. If you just use an HTML rendering engine you get x-plat, standardized stuff, including all of complicated stuff like support for assistive devices, internationalization, etc. If you use something like Capacitor or Tauri you get a clean model for building the UI (including nice things like a clean async/await that is built to support the UI thread properly). If you build a Hello World app with Capacitor or Tauri, it's comparatively tiny - a Hello World Tauri app can be less than a single megabyte.
Now, I'd like to address a few canards. First, is HTML/CSS/JS slow? Well, in a word, no.
- The HTML layout engine is slow. You wouldn't try to animate something using the Microsoft Word layout engine, and you shouldn't rely on the HTML layout engine for anything performance either. Here's a great example.
- It's extremely easy to write bad code and pass it off as the platform's fault. I can't count how many times I've seen people complain about the performance of a browser app (or Unity, or Godot, or...) and yet look at you blankly when you mention any kind of performance analysis tooling. Note that it really doesn't matter the stack - it could be Chrome performance analysis, Unity, or Godot, or whatever. If you are building an ordinary business app and you are having a performance problem, it's pretty likely it's your code and not the underlying stack.
- The secret to a responsive UI is almost always proper threading support. As of this writing, async/await is probably the cleanest version of this, and (IMHO) it's unfortunately more complicated than I wish it was. This is right up there with "I don't actually use any performance analysis tooling" as a common gap in developer experience.
Just to make the point clear, check out the number of jobs posted for:
Good luck out there.