It's all about the baseline. Baseline declarative has "good enough" performance while the same money/time investment into imperative UI will get you worse performance. It's only when you've poured in a lot more resources into the imperative version that you start to see it overtake the declarative version.
This problem is illustrated well by InfernoJS. When the JS framework benchmark was first released, the vanilla (imperative) version blew away all the frameworks on the list.
InfernoJS came along and beat the vanilla performance, so they updated the vanillaJS version. They went back and forth a few times with InfernoJS constantly upping the game and them looking at what Inferno was doing to get ideas into improving their performance further.
Today, the vanilla version is faster, but only by a little and only after a bunch of rewrites. Meanwhile, the InfernoJS declarative code didn't change a huge amount. The underlying framework did most of the updating. This means that Inferno would be fast on a second project while vanilla would once again start off slow and have to find ways to optimize their specific app all over again.
Only the most demanding companies and legacy apps will continue with anything other than SwiftUI once it has improved a bit. Saving all those dev man-years while giving up just a little bit just isn't a tradeoff that most companies (even big ones) are willing to make.
I'm not familiar with Inferno JS, but it appears to be based on the DOM, which is fundamentally declarative (i.e., flow-based layout rather than coordinate-based layout)? I.e., correct me if I'm wrong, but that doesn't sound like a pure enough way to compare declarative vs. imperative if it's built on a declarative foundation?
Yes and no. The DOM is declarative internally, but the fastest way to use it is imperative where you manually create nodes one at a time, update properties on those nodes individually, and string them together one command at a time.
The effect for the programmer isn’t so different from a series of draw calls except that it’s more sensitive to individual updates tanking performance which requires even more consideration from the imperative programmer.
To me, the best comparison is between an AOT language with manual memory (eg, C) vs a JITed language with garbage collection (eg, Java). In theory, the JIT can provide equivalent or even faster code, but that’s not generally the case in practice. The GC isn’t generally faster, but it is safer and easier. Some companies need their software written in C, but Java is good enough for most given the productivity gains.
In theory, runtime inference could dynamically improve performance of declarative systems beyond what AOT imperative code can do, but in practice, companies don’t invest the resources to make this happen. At the same time, not having to manually manage the output to ensure garbage doesn’t creep in accidentally greatly improves developer productivity.
This problem is illustrated well by InfernoJS. When the JS framework benchmark was first released, the vanilla (imperative) version blew away all the frameworks on the list. InfernoJS came along and beat the vanilla performance, so they updated the vanillaJS version. They went back and forth a few times with InfernoJS constantly upping the game and them looking at what Inferno was doing to get ideas into improving their performance further.
Today, the vanilla version is faster, but only by a little and only after a bunch of rewrites. Meanwhile, the InfernoJS declarative code didn't change a huge amount. The underlying framework did most of the updating. This means that Inferno would be fast on a second project while vanilla would once again start off slow and have to find ways to optimize their specific app all over again.
Only the most demanding companies and legacy apps will continue with anything other than SwiftUI once it has improved a bit. Saving all those dev man-years while giving up just a little bit just isn't a tradeoff that most companies (even big ones) are willing to make.