lukejacksonn / perflink

Low friction JavaScript benchmarks that you can share via URL
https://perf.link
1.21k stars 43 forks source link

Performance Breakdown and Bundler DX/UX/Perf Validations #15

Open TheLarkInn opened 5 years ago

TheLarkInn commented 5 years ago

So since the goal here was to not use a bundler, I couldn't help but bite on the opportunity to validate a few hypothesises.

(Spelling error disclaimer I'm a terrible speller feel free to edit for readibility)

  1. Does avoiding a bundler cause less web performance gotchya's? In a [now deleted] google doc, a particular browser team had made mention to "bonzai web fundamentals" in which they attempted to slowly add new technologies to the web platform in hopes to "remove the prolific usage of bundlers". Their theory was that if people bundled less, they would wind up with less Web Performance pitfalls. I'd like to use this small app as an example to answer this question.

  2. Does bundling beat out the native module case? I was impressed by the base performance for this application, and I wanted to make sure: "If I bundled, could we achieve consistently faster load times"?

  3. Is the barrier to entry lower by bundling? I'm not sure I can totally answer this because of my maintainer bias but we will attempt to get some ground here.

So lets move on to the validations:

First off, with this really awesome architecture, the webpack configuration is pretty tiny! (So awesome inspiration here 👏 )

image

It was quite nice to be able to not need babel, jsx, and friends and to stay true to the controls of the application, I used the production version of react-es, and there were no changes really to the index besides importing the CSS (it will be extracted anyways so lets move on).

image

One of the first opporuntities I noticed was found in the % of unused code in the initial bundle of the current perf.link app:

image

Overall its not the worst! Given the scale of the application, I'm not surprised to react-doms synthetic eventing (preact would kill here), editor.js, prism, and GTM are heavier unused quantites.

The sections however I expected to be lazy-loaded by default however were the editor and the display panels (behind the intro screen).

By lazy loading these and also prefetching those assets (because they are available at build time), it opens a cool opportunity to conditionally load the panel based on the started or !dialog state.

image

image

This did expose a strage bug where I had to convert init to a function so state wouldn't switch back and forth and hide the panels. Overall the home page looks almost identical however we are now taking a significantly large library (prism) and now conditionally loading based on whether or not the Tests or Results pages are open (and lazy-loaded on demand). We accomplished this by moving the imports of Prism to the Tests component itself.

image

Perf numbers side by side:

Perflink

image

First Contentful Paint (interactivity): 180.62ms

image

3G First Contentful Paint (interactivity): 4438.94ms

image

206KB of 394KB of JavaScript/CSS are not used (59%)

Perflink "Lite"

image

First Contentful Paint (interactivity): 82.26ms

image

3G First Contentful Paint (interactivity): 2050.09ms

image

144KB of 234KB of JavaScript/CSS are not used (62%)

What resonate with me the most is how fragile native modules performs under poor or low network conditions. Bundler here outperformed by a surprising amount (surprising to me at least). I'm not surprised by the extra bit of unused code because we have reduced the overall code shipped by a fair amount.

Overall after doing this perf analysis I think I've come up with validations for each of my hypotheses:

  1. Does avoiding a bundler cause less web performance gotchya's? Even in this small example, I think I'll have to give a definite "NO". What was perceived as being a prevalent cause (bundling recklessly including lots of modules), really doesn't turn out to be the main gotchya, rather knowing when and where you should be importing specific modules in the native module graph.

  2. Does bundling beat out the native module case? Definitely. I think you did a spectacular job covering the trade-off's in your article but I'll state it here: a lack of static analysis and pulling from packages on places like "unpkg" really hurt the ability to have "tress shakable", "minifiable", more optimized code. I think this is already a given, but now the numbers really back up how important it is. Would pika help here? Potentially! Something worth trying!

  3. Is the barrier to entry lower by bundling? I was pretty validated to find that it only took me 20 lines of webpack config and only a few packages to get this whole experience installed. Build times (for production) were only 900ms on a cold cache and roughly 850ms on a warm one! I think if you want to live in a buildless world, @lukejacksonn's approach here really hits home as a glimmer of light for it.

My challenge to the author would be to consider a format that lets you enable your bundler for production. Since performance is critical UX feature that we often ignore due to our technology privileges, we should try and avoid as many pitfalls or downsides in the platforms implementation of native modules.

PS: I am happy to PR my work, however I think it would be more fun to leave side by side in someway. I could also spin up a separate repo and we can have them in a comparison format also!

~ Sean <3 webpack team

lukejacksonn commented 5 years ago

Interesting read! Thanks for taking the time to conduct the experiment and for the various compliments.

TLDR; I think the outcome of the experiment is rather unconvincing. The gains are not very substantial and they come with a significant developer cost associated with them. I don't doubt that Webpack adds value in some situations still but here it does not. DX/UX tradeoffs are hard to justify.

Aside of what I wrote in the article "Don't build that app!" – which form our private conversations I presume you have already read – I have thoughts myself relevant to this particular scenario. I will try relay them appropriately in response to your own points:


if people bundled less, they would wind up with less Web Performance pitfalls.

This is a wild claim and sounds like marketing spiel or cliche cult hogwash (which is probably why the reference document was deleted). I can see many ways to cause perf pitfalls using both bundler and es module approach. It is almost entirely down to how the codebase is architected.

Perf degradation (in the context of bundling) usually come from not breaking up your files enough, breaking them up too much or requesting too much at one time. Webpack claims to help do this for you with smart defaults when it comes to chunking but that ultimately requires configuring at some point to get right. It abstracts some complexity but the working and output is not always intuitive.

With an ES module approach, the control (and ultimately the blame) is entirely in the hands of the developer. You know exactly what is loaded, when and by whom. There is no safety net (or illusion thereof) yet it remains a very approachable task; with sensible application of static and dynamic imports you can effectively scale a codebase to any size without significantly impacting perf.


If I bundled, could we achieve consistently faster load times

I have no doubt that this statement will remain true for a long time, but at what cost I ask? You highlight the benefit of ~100ms shaved off the first meaningful paint but you forget to mention that this approach (done properly) adds to the developer environment:

Not the end of the world but some of this is extra baggage and some reduces the portability of the project; complicating deployment to to github pages for example. Not to mention the time penalty incurred by the developer setting up the app and then every time they hit save. Admittedly having CI (if you have any) or a pre deploy hook to run this just once for production is much more reasonable.

What strikes me as backward here though, is that; the browser is able to fetch files from the internet, parse them, evaluate them, fetch sub dependencies, link them all and render the app in almost one sixth of the time it takes Webpack to compile the project locally.. and it does all that with no overhead.

This trade off here seems unreasonable, especially considering this is that this site is admittedly very fast in the scheme of thing – certainly not slow enough to ruin the UX – and the codebase is so small.


Is the barrier to entry lower by bundling?

I have no maintainer bias here and can confidently say NO it is certainly not. How can it possibly be? Ignoring the hard drive space and bandwidth required for the dependencies you need to install for every project. You needed to know more than a few things to get started:

This is all before even writing a single import statement! It is worth emphasising that in this case specifically we are going to demand all this in exchange for ~100ms of load time!

The 20 lines of config, 40MB of dependencies, ~900ms wait on builds and a wealth of domain specific knowledge is a barrier to entry however rose tinted your spectacles. Not to mention the burden that not having an standard module definition puts on library authors and maintainers who are having to build for multiple targets like umd, amd, cjs, etc.

Summary

I chose this architecture because it was easy. I did not lead myself down the garden path thinking this was the optimal way to require everything needed for the UI but it certainly proved sufficient and is by no means slow. I will experiment with loading only the code for the overlay and then dynamically importing the tests and graph components when !dialog is true. I would hope to see some improvement in first paint times, perhaps similar to those you demonstrated here (even on 3G).

One thing that the platform still hasn't figured out which is clearly solved by employing a bundler is being able to import CSS effectively. There is a proposal for this somewhere I believe but in the mean time there are some interesting runtime solutions; I have been working on one of my own for example csz (which is still very much a proof of concept) but I would like to see more happening in this space.

The phrase "Using a sledgehammer to crack a nut" comes to mind when seeing the overhead created by adding Webpack to a project like this. Perhaps that is down to my personal preferences though.

So thanks for the comparison but as it states in the README I am not interested in:

Admittedly I am somewhat of a purist (and making things as lean and efficient as possible is a passion of mine) but waiting for Webpack builds that take way over a minute has given me the patience to accept one tenth of a second load time in exchange for a frictionless developer experience that can scale. I'm sure whilst growing the app I will undoubtedly find pitfalls of this approach but I welcome them (if not for anything else) to benefit my own understanding.

I really admire your dedication to Webpack. So I hope you can appreciate that I am going to stay true to the project values here and try contribute to ironing out the general issues with ES modules on a platform level instead of a employing and deferring responsibility to a proprietary solution like Webpack.

All in all, very informative and inspiring so thank you again 🙇

P.S. This whole conversation did get me thinking and there is one more option I would like to explore and that is somewhat of a middle ground which is pikapkg; it could mitigates a few of the issues with a purely es module approach you outline here and is not quite as involved as Webpack. I would love to see all three implementations side-by-side (deployed) so that we can see how they all compare in setup and output.

FredKSchott commented 5 years ago

Happy to create a @pika/web test case to compare against all of the above as well. It is meant to be the middle path between the two, so hopefully it can deliver on both ease of use and simplicity.

(Context on @pika/web: https://www.pikapkg.com/blog/pika-web-a-future-without-webpack/ )

Some additional thoughts:

lukejacksonn commented 5 years ago

Thanks for the comment and advice @FredKSchott. I'm still new to pikapkg but am excited to see how a setup works and performs against the ES modules and Webpack only approach 🙌

I too am interested in the potential of optimising at deploy time (particularly for projects larger than this one) but have been thinking recently, what if we take this one step further and do it at serve time? Perhaps this is wishful thinking and we should take one step at a time. Plus it does mean that you couldn't rely on a simple CDN to serve your app anymore but it would be cool to see.

Think something like https://github.com/phaux/es-serve which does babel transforms on the fly, or gzip encoding; a serve time optimisation which already comes as standard.

I think unpkg.com are already doing something like this when you try import *?module.

FredKSchott commented 5 years ago

Okay, I finally had some real time to sit down with this. Adding @pika/web was pretty painless, except one gotcha (no one is safe!) where @lukejacksonn's packages didn't export a "module". With only a "main", @pika/web assumed by default that the package wasn't ESM. You can get around this by pointing to the file directly, but I was hoping to show @pika/web without even needing the whitelist.

Screen Shot 2019-04-07 at 9 57 50 PM

@TheLarkInn I was traveling when I first read through this, and hadn't realized how much optimization you had to do in the OP to get your final result (got rid of the dynamic es-react vs. es-react-production dependency picker, added some markup to the React.lazy calls, etc.). Drawing ANY conclusions between any one tuned, optimized site vs. one unoptimized site seems futile at best, and disingenuous at worst 👹

On top of that... it looks like you're testing your bundled "Lite" site on localhost, against the remotely hosted https://perf.link (forgive me if I'm wrong, the screenshot is small but I do see 127.0.0.1). Again, drawing ANY conclusions between any one locally hosted site and one remotely hosted site seems yadda yadda yadda 👹

I'm happy to post the numbers I got with @pika/web (they come in between the two examples above) but given the flawed OP stats and the fact that @TheLarkInn is trying to draw some pretty sweeping conclusions from all this, I'd rather do it right and get all three examples (control + webpack + @pika/web) hosted somewhere that anyone can bash on.

Most importantly: I'd like to test with CPU throttling turned on, a more realistic 3G scenario missing from the OP that I'm really curious about.

@lukejacksonn do you have any interest in organizing? @TheLarkInn can you share your fork?@pika/web flavored perf.link: https://github.com/FredKSchott/pika-perflink

TheLarkInn commented 5 years ago

Yes I'm more then happy to provide the fork! It may take a few days as I have some other work priorities but yes.

TheLarkInn commented 5 years ago

Couple days late sorry but I got my fork finally pushed! Mind you I really tried to do as little as possible here so that 1: @lukejacksonn's code was damn near identical 2: Wasn't creating some elaborate webpack config etc. 3. I don't have time yet to engage in some of the responses for these discussions above, however I have a lot of good thoughts on them!

Have fun looking at the repo and feel free to extend, change, benchmark against, etc. Heck for a more representative scenario, having it deployed somewhere will atleast give less people to complain about perf wise.

Here is the branch comparison for a quick diff but you should be able to get to the repo from there

thepassle commented 5 years ago

Hi ya’ll 👋

Did you guys ever get round to having the 3 different setups hosted? Would be wildly interested in seeing the results. Also happy to help out if I can, lmk.

lukejacksonn commented 5 years ago

I think all versions are now on GitHub.. you could clone them all into a directory, install and build then run zeit now or something similar in all three to see how they compare. I'd love to see the results too!