Chrome has taught us to idealize features for so long that we've become blind to its many glaring faults.
No browser is perfect, this is an unfortunate situation we must live with. But ranking my experience developing responsive hybrid applications, Safari gets an A- and Mobile Safari a B+, while Mobile Chrome get's a D on a good day. Overall my rankings currently look something like this:
- Safari: A-
- Edge: A- (tentative, I haven't worked with it often enough yet to honestly rank it)
- Mobile Safari: B+
- Firefox: B+
- IE11: B+
- Opera: B
- IE10: B
- Chrome: B- (on a good day when I'm happy with how nice their dev tools are)
- IE8/9: C-
- Mobile Chrome: D (on days I'm not cursing them)
If this ranking rankles you; it should. Hasn't Chrome been the best browser basically since its inception? Isn't it always the most standards compliant browser? Doesn't it offer the most access to experimental features like
requestIdleCallback? How does Chrome deserve this ranking?
Maybe we've idealized features for too long .
Features, even features that help us build faster, more capable, more powerful web applications are only great if they work well. And herein is the problem; Google implements features that (usually) work at a high pace, but they very rarely make them work efficiently.
Chrome has had such a problem with performance they formed a special group just to work the problem, but in nearly two years time that group has yet to produce anything tangible. Instead, they introduced us to shiny few features like CSS
requestIdleCallback, and a fairly solid implementation of
ServiceWorker in the hope that new tools would magically make their performance gap go away. It doesn't matter to them that even these shiny new tools are 3x+ slower than their peer's counterparts.
The end result of this is that we've been brainwashed into believing Chrome is the best browser, when the reality is that across all metrics, Chrome is 3x to 300x slower than Safari. The situation is dire enough that even desktop Chrome often gets beat by mobile Safari in performance benchmarks.
Ok ok, I get it, yada yada yada, Chrome is really slow, so what?
Recently, you might also have been lulled into thinking that Apple hates the web, or that you can only build rich app-like experiences on Android.
To me, these positions only exist because Chrome's performance is so bad we've subconsciously altered our expectations.
And I don't mean to hate on Henrik too much, he makes a lot of very good points. I do wish that Apple upstreamed WebKit's improvements into stable Safari Releases faster.
I absolutely agree with his statement that:
The web is the only truly open platform we’ve got. It’s the closest thing we have to a level playing field.
And with Vjeux's position that platforms like React Native are needed to push browser features and capabilities forward (keyword, browser).
So with all this in mind, I want to address a few claims floating around Twitter, blog posts, and Hacker News that I definitively believe to be wrong. But first, here's my experience building for Mobile.
How to build good Mobile Apps
- keep your JS/HTML/CSS payload under 1MB
- Keeping the JS payload below 750kb seems to be the key point for Android,
but keeping below 500kb is more ideal.
- Mobile Safari can honestly handle 2-4MB apps without blinking, yes, the parse and compile performance differences are that severe.
- complicated, verbose CSS is just as bad as complicated verbose JS
- Keep your DOM svelte
- You want to keep a fairly low DOM node count, I target <5k nodes, ideally peak at <10k nodes and I make heavy use of occlusion and recycling to achieve that.
- Chrome leaks memory used for DOM. It will reuse memory allocated for DOM, but it does not release it, even on tab/page reloads. High memory usage slows everything else down a huge amount.
- Keep a low memory profile
- I tend to find that once the JS Heap size is heading north of 30mb, I'm likely heading for trouble. 50mb seems to be the upper limit for keeping everything snappy.
- To hit this goal with ambitious apps, you often need to remove data not used recently from memory and leave it in either a storage solution or a WebWorker. WebWorker memory usage seems to be able to withstand bloat without slowing down the main app.
- Be very careful not to create too many GPU layers, especially nested ones.
- Get a smarter build pipeline
- Ideally your build pipeline implements some form of tree-shaking to shave off dead/unneeded code, and I hope I don't need to remind you to minify js/html/css, and optimize images.
- Corollary: know your dependencies, don't install large ones just to use a few minor functions.
- Use WebWorkers to do the heavy lifting
- network requests, complicated state changes, particularly expensive client side sorts, your data layer: these are things that belong in workers.
- if you are clever, you can pre-fetch images and pre-render some parts of your application within the worker too.
- Coalesce network requests
WebSocketsas much as possible
- even where not possible, coalesce requests together, every request represents a major potential point of failure or blockage
- Embrace "no work"
- in Ember, computed properties and service injections are lazily evaluated
- Ember-cli makes it fairly easy to make a lot of optimizations at "build time"
- scheduling work into a queue system built over
requestAnimationFrame(especially when using
debounce/throttlelike mechanics) will help you do your work smarter, and
- Organize your DOM access
- building queues that specifically handle DOM reads and writes of various forms can dramatically improve your render experience, especially when the queues are tied to
- Data Down, Actions Up
- This data model paradigm is about much more than simplifying how you think about data flows in your app; simpler data flows make it much easier to optimize bottlenecks, and reduces the risk of triggering multiple re-renders on an update.
- helps your app avoid situations in which it needs to perform the same work more than once in a render cycle.
- most importantly, it makes it easy to optimize network requests at the top.
- Cache. If possible AppCache or ServiceWorker, but even just making sure your static asset server is properly setting cache headers is important.
When you follow these rules, the apps you ship will run with native-quality performance on Mobile Safari. But what about Android?
Well... that depends on how closely you followed these rules.
Let's say you followed these rules 0% of the time, you'd probably still have a solid app experience in Mobile Safari, but even the most powerful Android phone with the latest browser is going to have such a miserable experience that the user bails before the loading bar has even started to budge.
Let's say we did a good job though, and followed these rules 90% of the time. And let's pretend, for a moment, that we didn't know very much about the relative browser compatibility of various JS and CSS features. Let's pretend (ok, maybe not pretend, this is what we do) that we didn't even look at our site on a mobile device until development was done, that we did everything in Desktop Chrome.
We boot up Safari, and get a white screen on our login form. o.O After 2s of googling it turns out the error we got was because we were attempting to set a disallowed property on a form input, an error Chrome had simply swallowed. There's a lot of these errors that Chrome silently ignores or just "deals with", and it leads to code debt that we "think" is due to other browsers being shitty, but honestly it's just what I've come to call "Scumbag Chrome".
Error resolved, our screen renders, and the layout is messed up all over the place. Why? You probably used flexbox, and wether you know it or not, flexbox is not only an incomplete not-actually-standardized-yet thing, but it's existed in the wild in 3 very different spec forms over the past 6 years, and not one browser has a non-buggy implementation of it yet. Cross browser flexbox is tricky, but it is doable, and for Safari it takes a few minutes of prefixing some things (potentially this involves just making sure you are using autoprefixer). Done, moved on, next?
That's probably it. For the most part, on Safari, things just work. You'll likely have to use a different data-storage mechanism (likely WebSQL instead of IndexedDB) for localForage or PouchDB, but for the most part these are simple to find, debug, and accomplish one time tasks. On a large project, I find I spend a few minutes to an afternoon in total dealing with Safari quirks.
Awesome, you put down your iPhone and pick up that Android burner you bought just for this project. And it's bad, really bad, painfully bad. Everything seems to work, but nothing is snappy or responsive, and initial load and render makes even you (the builder) want to stop the page load and head off to Twitter instead.
No problem, you think, this is a pretty shitty cheap phone. Let's try a nice phone! You find the guy in your office with the super-new-cool-blazing-fast-better-than-any-iphone-Android, and launch the site. It loads, but it takes a few seconds. Things are a bit snappier, but everything is still noticeably slower, or lag prone. Your friend boasts about how great the phone is, but having seen this same site load and run effortlessly moments ago on your old iPhone4s, you chuckle inside, put the phone down, and head back to your desk in despair.
This was my story, it's the same story I've seen play out dozens of times now. I've spent a ton of time building Open Source tools so that we can build ambitious JS apps more easily and not have this happen to us.
Probably. But before we get to that, let's finish our story of building the app.
Having seen how bad Mobile Chrome is even on the best device on the market, you put your head down, pop open the developer tools, and get to work figuring out what exactly is going wrong. After a month you've corrected that 10% of the above rules you forgot to follow initially, and the site is noticeably better, it loads (slowly) now on the burner phone.
Another 1-2 months, long after having delivered an amazing app experience for desktop and iOS, you've spent what feels like a lifetime CPU profiling, memory profiling, and eking as many kilobytes off of your build as possible, you start to give up. The performance has gotten better, but it just feels like you could have built a stand-alone native app by now, yeah?
Then you hear about SSR (server side or universal rendering). Magic Bullet! YES! You spend another month preparing your code for SSR, get it running, and... the app starts to render the screen and then suddenly pauses. All your original assets were delivered and parsed at a really inconvenient time. You figure that out how to avoid that, now the app is just a wee-bit faster. Initial render feels comparable to iOS, many things are still sluggish though, and there's that long period right after the initial render in which nothing works.
Then you hear about
ServiceWorkers (SWs), (were you living under a box? maybe, but a lot of people haven't heard about them). A
ServiceWorker can cache your assets! And your data! And your images! Maybe even some of the initial html! Excited, you rush out and learn about this new amazing technology (p.s. it is new, and it is amazing, and it will become the foundation of a ton of new best practices, app patterns, and feature rich applications). After a few weeks of figuring out how to build a decent SW, and a few more converting your app to be able to take full advantage of "app-shell" architecture and SW cacheing, you breathe a sigh of relief. The initial load and render time is now on par with iOS.
In App things still feel laggy from time to time, you had to turn off some animations because you never could optimize them, but it's a decent app experience, and you are happy with it. And it's awesome that Chrome had this feature, right?
If that last thought was actually your last thought... congrats, you just mentally accepted years of torture doing exactly this to fix the other "most feature rich browsers"... IE6, 7, 8, and 9. ...but "Standards!". Yes, these features are on standards tracks, and thankfully far enough along they aren't likely to be rejected. But guess what, a lot of IE's ideas were proposals too, and a lot of them are only just now being accepted as standards today, just under different names and with improved APIs. Being first does not make you the best.
I've learned the hard way that Chrome is the new IE. I've learned that you have to architect an application well from the beginning for it to work well on all platforms. I've learned you can ship large ambitious JS apps to mobile, but it takes dedication and experience, and every trick you know to do it well for Android. I've learned that Apple loves the web, probably more than Google, and has invested heavily in ensuring we have a high quality platform upon which to build apps.
But most of all, I've learned that we're wasting a ton of effort right now trying to fix Chrome from the outside. We're dancing around the issue; pretending that universal rendering, service workers, app-shell architecture, and keeping more of our applications on servers (where they don't belong) is more than just a workaround for how bad Chrome is. Yes, these ideas have uses, merits, and probably are the future; however, our need and love of them right now is because our performance expectations have been badly distorted by the situation Chrome has left us in.