16 ms·
Speeding up the JavaScript ecosystem, one library at a time
- fabian2k 4y agoMy own experience is that JS can be really, really fast if you manage to avoid confusing the JIT. But it's also very easy to make stuff slow and it can be hard to guess what kind of stuff will be an order of magnitude slower because it avoids a fast path.
- dottedmag 4y agoAnd that's why profiling is the step 1 for any optimization.
- maccard 4y agoWhich is why this article is so good IMO.
- Yahivin 4y agoIn other words: JavaScript is fast, JavaScript developers are slow.
- hajile 4y agoMore like JS doesn't do anything to discourage writing slow code. Typescript should have been that tool IMO, but instead, it is so flexible that it doesn't do a thing to help developers understand that they are writing slow code.
- klysm 4y agoI appreciate typescripts stance to be a non-optimizing compiler. Having two optimization layers would make things weird
- The_Colonel 4y agoI don't think that's what the other commenter suggested. The point is that TypeScript should disallow (or at least make harder) to write code which is hard to optimize by the VM - e.g. allowing function parameters to be of multiple types. But TS has a design constraint to be essentially a superset of JavaScript so it would be difficult to realize this goal.
- k__ 4y agoI had the impression, my code gets less polymorphic when I use TypeScript.
- hajile 4y agoIf only. This is polymorphic because union types are polymorphic. let stringProcessor = (foo: string | number): string => { //do stuff with foo return "string" + foo } It's super easy to make non-optimized arrays and objects this way. type MyFoo = string | number //somewhere in the codebase //you will get zero hints that this is a polymorphic function let formatFoos = (foos: MyFoo[]) => foos.map((foo) => `${foo} deoptimized`)) Any use of optional object properties also immediately leads to polymorphism too. I doubt you can find ANY significant TS codebase where this isn't used pervasively. interface Foo { foo: MyFoo //polymorphic bar: string baz?: number //this is polymorphic too } let doStuff = (obj: Foo): string { //do stuff with obj return "string" } doStuff({bar: "abc"}) doStuff({bar: "abc", baz: 123}) //we're now polymorphic Even if we remove the `?`, it can still be polymorphic because the TS compiler doesn't reorder object properties when compiling. interface Foo { bar: string //all basic types, so we're good? baz: number //no more optional, so we're fine right? } let doStuff = (obj: Foo): string { //do stuff with obj return "string" } doStuff({baz: 123, bar: "abc"}) doStuff({bar: "abc", baz: 123}) //we changed order, so now we're now polymorphic
- brundolf 4y agoI'm very surprised the property ordering would matter. Seems like the JS engine itself could easily fix that, no? Silently re-order the properties to fit its own optimization Do you have a source for the above info? (both because I'm a little skeptical of some parts of it, but also because I'd love to learn more about this topic and I've had trouble finding concrete info in the past)
- croes 4y agoJust because it can be faster doesn't mean it's fast.
- hajile 4y agoWhen you get down to basics, JS has arrays, structs, ints, floats, and strings+regex (both backed by very fast C++ implementations. If you stick to these things as if they were C analogs, the performance potential is very high. Viewed in those terms, it shouldn't be surprising that JS can be incredibly fast (faster than C in some cases).
- maccard 4y agoNo, it's possible to write dodgy code in other languages too - 2 of the examples in this article are in go rather than JS
- deleted 4y ago[deleted]
- cxr 4y ago> But it's also very easy to make stuff slow and it can be hard to guess what kind of stuff will be an order of magnitude slower It's not that hard to guess. Just look at what most NPM/Electron programmers and guides recommend you do, and then do the opposite. I.e., think about what your program is doing, and write the most boring, 90s-style code you can. Works really well internally for the Firefox codebase, and has been for 20+ years (for longer than it has even been called "Firefox"). Relevant talk: <https://www.youtube.com/watch?v=p-iiEDtpy6I https://www.youtube.com/watch?v=p-iiEDtpy6I> Relevant paper: <https://users.dcc.uchile.cl/~rrobbes/p/EMSE-features.pdf https://users.dcc.uchile.cl/~rrobbes/p/EMSE-features.pdf> Relevant thread: <https://news.ycombinator.com/item?id=33790568 https://news.ycombinator.com/item?id=33790568>
- hajile 4y agoFor those who don't want to go through all that, there's basically 3 simple rules to make your code optimizable: * Objects should have a fixed set of keys (no adding more and especially no `delete`) and the type of the value should never change (note: in JS, a type must NOT be a union of multiple types if you want it to optimize) * Arrays must be of one type and a fixed length * Functions must take a fixed number of arguments and the type of those arguments must never change. This is by far the most important if you want to get your function's code optimized. And as a corollary from this article, it's not JS specific, but converting string -> float and float -> string isn't a cheap operation. I'd go so far as to say that it is the most expensive operation in a huge amount of the functions where it happens. Int to string is especially egregious because it requires a lot of division which is basically the slowest (common) thing you can do on a CPU.
- esperent 4y ago> Arrays must be of one type and a fixed length If performance is critical then use a TypedArray where possible. Your other points are spot on for high performance JS.
- crabmusket 4y agoI was messing about with some profiling and seemed to find that TypedArrays are not always faster than regular arrays carefully handled. And what's worse it seemed like a regression from node 16 (fast typed arrays) to 18 (slow typed arrays). I will have to dig up my profiles again and see if I can verify it. Posting this comment in the hope that someone who knows more than me has something to add!
- AtlasBarfed 4y ago"really, really fast" Compared to what? Polar bears? Here: is it faster than warm JVM?
- winrid 4y agoV8 is almost never faster than a warm JVM, especially once you get to heap sizes bigger than a couple GB. You can't even store more than 14m or so keys in a map in Node w/o reaching for an off heap solution due to hard limits in the runtime...
- fabiospampinato 4y agoIf needed you can make your own map that doesn't let you down: https://www.npmjs.com/package/infinity-map https://www.npmjs.com/package/infinity-map
- winrid 4y agoI've used megahash with great success: https://github.com/jhuckaby/megahash https://github.com/jhuckaby/megahash
- ptrwis 4y agoI would freeze before the JVM would warm up.
- winrid 4y agoJokes aside, then maybe your code cache size is too small ;p
- mhagemeister 4y agoAuthor here. True, optimize code for JS engines is certainly an art form of its own. What usually works for me is to write boring imperative code when performance is a requirement. That seems to be easily optimizable by current engines. Also, thanks for sharing my article here on HN. First time for me one of them is on HN.
- avh3 4y agoI enjoyed reading your blogpost. Thanks for sharing!
- Joeri 4y agoOr … just don’t use that stuff. Postcss is not necessary for custom properties. IE isn’t a thing anymore, and using css custom properties directly without a transpilation step is fine. The same thing applies to the for … of issue. Babel is not necessary anymore, all browsers support ES8. JSX is the only thing babel still does that offers real value, but that can be replaced by the runtime alternative developit/htm. We can just stop using babel and sidestep this whole issue. I see a lot of energy going into making faster build tools, and I feel this energy is misdirected. We need to dramatically strip down the build tools as most of them have become largely redundant since the death of IE.
- 9dev 4y agoI share your opinion, but recently got very disappointed to find out that safari still needs lots of -webkit- prefixes; even for stuff seemingly available for ages now. So we’re not fully out of the pit yet, at least until the most valuable company in the world finally cares to fix their fucking browser (yes, dear Apple engineer, I’m talking to you!)
- _hyn3 4y agoApple's app monopoly gets stronger when the browser gets weaker.
- CharlesW 4y ago> recently got very disappointed to find out that safari still needs lots of -webkit- prefixes; even for stuff seemingly available for ages now. Safari is fairly conservative in de-prefixing new features, but also sometimes beats Firefox (e.g. Container Queries and Container Query Units). For reference, here's the caniuse.com list of Safari CSS features which need prefixing, compared to Firefox: https://caniuse.com/?compare=safari+16.1,firefox+107,ios_saf+16.1&compareCats=CSS https://caniuse.com/?compare=safari+16.1,firefox+107,ios_saf...
- easrng 4y agoIt feels kinda weird but a lot of the time you can just use the -webkit- prefixed versions and they'll work in firefox too.
- cogman10 4y agoHeh, so many of these fixes boil down to "I'm converting this thing to a string and then searching the string". And, funnily, I've often stumbled on this problem with Java programmers. They'll take a nice rich type, turn it into a string and then spend a bunch of code/time turning that string back into something else. Super wasteful when you start thinking about what all that means. Rather then having a representation that neatly fits into registers, now you have this blob of memory that has to be linearly searched to pull information from it. Just. yuck. And why do devs do this? TBH, I really don't know. Because regexes are easier to understand? IDK.
- Waterluvian 4y agoI’ve never seen this approach. I imagine I’m just not working in a domain that touches on problems “solved” this way. Any popular examples to point to?
- cogman10 4y agoNope, just stories from my day job. One time, to solve the problem of "we want floating point numbers that are close to be considered equal" on an object, the programmer decided to take every field of that object (reflectively of course) turn that into a string, and then compare the two strings together. Another time, in order to convert a LocalDate to our own internal date representation, a dev decided to toString the local date, split on the `-`s to extract the day month year. (Yes, localdate has day, month, year methods). One time, to convert an int to a double a dev decided to turn that int into a string and then used "Double.parseDouble" to turn that int into a double (I wish I were joking). All these and more were found through the joys of profiling :D
- Waterluvian 4y agoI’m in pain reading this. But I feel the need to confess my own sin: I once didn’t want to write or add a dependency for a Python deep equality checker so I just sorted, JSONified, and string compared the two. It worked fine for years. It wasn’t performant important code. But it still keeps me up at night.
- pfych 4y agoIt's crazy that their patch in Semver (something that doesn't really affect the final build) saved them 4.7s in their build! I never would have thought a package like that would have such an affect on build time.
- mhagemeister 4y agoAuthor here. Yeah, that was surprising to me too. You always discover something you would have never thought of on your own. It's what makes profiling such an enjoyable task for me!
- flanbiscuit 4y agoI'm curious to see the project that you tested this against. To me it seems like you either tested this against a rather large existing code-base you work with or you code-generated something pretty huge as I've never seen such slow build times. If possible, could you put it up on github or gitlab?
- romellem 4y agoVery interesting, and great work contributing to the open source community! Can you elaborate on how you did the profiling to identify the (potentially) slow parts?
- llanowarelves 4y agoYes this would be helpful. JavaScript programmers, in my experience, don't really use profilers like "traditional" programmers do. Maybe it's as much the tooling as it is the culture?
- chii 4y agoThe profiler in chrome is pretty good, and can be used fairly easily for both frontend and backend profiling. If said programmer doesn't use it, it's because they lack engineering maturity and competency.
- brundolf 4y ago> it's because they lack engineering maturity and competency There's no need to escalate into personal attacks. Chrome's JS profiler is great, and is perfect for the kind of thing the OP is doing. But it's very rare that CPU cycles are a material problem in customer-facing JavaScript code, and it would be easy to get quite a few years into a career without ever getting exposure to it. I've had exactly one (very unusual) job where I used it frequently. The rest of the time, it pretty much collects dust.
- spoils19 4y agoIt's a reflection of their experience and tenure as a programmer, not a personal attack. The fact that it applies to most, if not all, JavaScript developers is a coincidence.
- brundolf 4y agoAnd I wouldn't fault a senior engineer's competency for not being familiar with JS profiling. Like I said, you could easily have a decade's worth of front-end jobs without it ever becoming relevant.
- seydor 4y agoDon't speed it up,just remove it. I just logged into AdWords after a long time, and OMG the page is 178MB of javascript and a clunky hell that surely turns a lot of computers to space heaters. This madness has to stop
- o_m 4y agoWith that logic just stop using AdWords. It bloats the web. Javascript isn't going away tomorrow, even if a replacement was released tomorrow it would take years for all the web to move.
- seydor 4y agoI will, i set an end date for the campaign and don't plan to use it ever again until i am forced to (and maybe alcohol will help) I'not arguing to remove javascript altogether, just to stop this bloat
- jongjong 4y ago>> Most popular libraries can be sped up by avoiding unnecessary type conversions or by avoiding creating functions inside functions. I stopped reading there. This performance overhead is insignificant. The way to speed up libraries is to keep time complexity low; e.g. don't search through every item O(n) when you could just have done a constant time lookup O(1)... Or don't have nested for loops with O(n^2) complexity when the problem could have been solved with two non-nested loops in O(n) time (e.g. using a Set or Object for lookup).
- srcreigh 4y agoThe article describes significant time savings from these changes.
- jongjong 4y agoAll the issues mentioned are related to the usage of shitty frameworks and build tools. The problem is not caused by a single regex; the real issue is likely that a regex is being created too many times in a loop instead of being created once and re-used. Related to neglected time complexity. > Config parsing as a whole takes 4.3 seconds WTF! If parsing a config file takes more than 20 milliseconds, you've got some serious issues with your project... There is no excuse. Even if your project is highly complex and feature-rich, you can just break it up into smaller sub-projects. This article doesn't give any useful info because the project it's analyzing is just so bad. It's like watching a turtle complete a 100-meter race and then taking away the lesson that crawling on one's stomach isn't a great strategy for a human to win the 100-meter race at the Olympics. Duh! The whole project is a giant flaming pile of garbage. There is no interesting lesson to learn from it. Functions named 'monkeys' or 'perItem' or 'strongRound'?... Class named 'ConditionalRuleset' (WTF is a conditional ruleset, why would any project need it??? This abstraction makes no sense). What about 'isBlockIgnored(ruleOrDeclaration)'? Sounds like unnecessary complexity bloat when devs keep inventing new abstractions to give themselves more billable hours of work. I would jump dump all these bloated libraries and frameworks and start the project from scratch.
- 4y ago
- jdmichal 4y ago// Does the same as `Number.prototype.toFixed` but without casting // the return value to a string. function toFixed(num, precision) { const pow = 10 ** precision; return Math.round(num * pow) / pow; } I know doubles are big, but doesn't this combination of multiply-and-divide introduce the possibility of precision issues that I'm assuming is not applicable to `toFixed`? Unless I'm wrong, I would be very hesitant to use this version of the method when I don't control the input.
- aeyes 4y agoThis is processing numbers which appear in SVG files which are basically coordinates, they aren't that big.
- chrismorgan 4y agoThis need not be true: you can put objects and viewboxes wherever you like in SVG. <svg viewBox="1234567890 1234567890 100 100"> is perfectly valid. Many years ago (Adobe SVG Viewer days) my Dad generated SVG files from geo/map data and at first used the original coordinates or something, but float error made it untenable—some viewers would end up quantising some things (e.g. the shape, the viewbox, individual nodes, even once you applied a transform or SMIL animation to them, the hover hitboxes—I don’t remember the particular details, but it was a few things) so they’d end up off by as much as hundreds of pixels in the most extreme projections. Therefore he ended up rezeroing to work around this, transforming into a more friendly space before producing the file. In the context of SVGO: really, the whole decimal places thing is stupidly bad, because decimal places can be of wildly differing importance in different contexts, and it wouldn’t be that hard to figure most of them out. In some situations a third decimal place will be more valuable than another number’s hundreds place. And SVGO’s approach is particularly bad because it treats each number individually, not attempting to compensate for the error it introduces, and so the presence of fine detail can easily completely destroy things due to untreated cumulative error.
- ygra 4y agoI wouldn't expect SVGs that require double precision to render anywhere properly. Most renderers these days use 32-bit floats simply because it's easier to get things to render on the GPU that way, so at very large or very small scales things tend to break in fun ways.
- ayhack 4y agoSpeed on roblox
- randompi 4y agoWhats that profiler being used?
- hajile 4y agoLooks like the one built into chrome. You can open your browser and attach a node instance so you can debug and profile it. Here's a basic How-to: * Run node with `node --inspect <file>` * Open Chrome and go to `chrome://inspect` * After it finds your target, click the inspect button to launch a debugger.
- alangpierce 4y agoIt looks to me like the profile viewer is actually speedscope ( https://www.speedscope.app/ https://www.speedscope.app/ ). I find it nicer for exploring profiles compared with Chrome's built-in viewer. To use with Node.js profiling, do the `node --inspect` and `chrome://inspect` steps, then save the profile as a .cpuprofile file and drag that file into speedscope. Another thing I've found useful is programmatically starting/stopping the profiler using `console.profile()` and `console.profileEnd()`.
- jansan 4y agoThat is a great tool. Also, I learned about perfetto the first time today, after doing Javascript profiling for a few years.
- mhagemeister 4y agoAuthor here. Answered this elsewhere here: It's the `--cpu-prof` argument with node and the resulting profile is loaded into speedscope.
- hajile 4y agoSucrase is faster than SWC and esbuild in single-threaded performance and something like 20x faster than babel. https://github.com/alangpierce/sucrase https://github.com/alangpierce/sucrase The real issue isn't JS, but is the lack of profiling (as shown here) combined with wanting to be everything to everyone leading to layers of bloated abstraction.
- pohl 4y agoAny idea how well does Sucrase compare when multiple threads are allowed?
- hajile 4y agoYou can run multiple processes of Sucrase if you want at which point it will still be faster than threaded SWC/esbuild. This is currently up to the user, but hopefully, the authors follow through on this issue and integrate an easier way for users to do this (though I don't know if they want that as core functionality). https://github.com/alangpierce/sucrase/issues/730 https://github.com/alangpierce/sucrase/issues/730 JS JIT creators don't get enough credit for their string implementations. Not much comes close when you consider both performance and robustness they packed into the C++ implementations that JS is actually using.
- chrismorgan 4y ago> The real issue isn't JS Yet it should be acknowledged that JS encourages various inefficient patterns. Comparing with Rust, with which I am very familiar, I think that you’d be less likely to have most of the problems (some a little less, some a lot less): • isBlockIgnored: it might still be tempting to do it the /…/.test(rule.toString()) way, but you’d be less likely to do it that way for two reasons: working with the actual AST is a good deal more pleasant than it is in JavaScript (due to a better type system, pattern matching and iteration tools), and you have to go a little further out of your way to use regular expressions since they’re not just built into the language. • strongRound: somewhat less likely, because its inefficiency is more obvious, and because the developer is less likely to even find format!("{:.1$}", data[i], precision).parse().unwrap() than +data[i].toFixed(precision). • stringifyNumber: similar remarks on regular expressions, you’re more likely to manipulate the strings rather than bothering with regular expressions (because they are a bother, even if minor, unlike in JavaScript). • monkeys: well, in this case the issue is JS; the whole thing would be a non-issue in Rust. • _loop: again JS problems and a misapplication of old-JS/new-JS–mixing techniques. • semver: in good design this would be somewhat less likely in Rust because it’s likely to shift the parsing invocations to the caller in a way that makes the inefficiency a little more likely to be noticed, but in practice this would probably be about as likely to happen in Rust as in JavaScript. And these are fairly tame cases, all things considered. Simplifying a bit, JavaScript makes it easy to do very inefficient things by accident, but Rust helps in many (though certainly far from all) cases, making it so that some of those can be optimised so that they aren’t inefficient after all, some simply don’t compile or can’t be expressed in the first place, and some must be expressed in slightly different ways that make the inefficiency more likely to be noticed and avoided. —⁂— You also should really specify that Sucrase achieves its speed by doing something completely different. SWC and esbuild are explicitly doing basically what Babel does, and so comparisons between them and Babel are meaningful, but comparisons with Sucrase cannot be used to vindicate JavaScript performance. Ports of what the likes of Sucrase or Bublé do to Rust or even Go would blow the corresponding JavaScript library out of the water.
- greggman3 4y agogood stuff but something was mis-transcribed. This line can't possibly work for (const i = data.length; i-- > 0; ) {
- chrismorgan 4y agoThe original used var for both i and rounded. let would work here.
- mhagemeister 4y agoAuthor here. Whoops, good catch! That's definitely an error on my part. The original code uses `var` there. Thanks for pointing it out.
- v0idzer0 4y agoReally fascinating read. Thanks for sharing.
- cjblomqvist 4y agoIt's it just me or why isn't the total build time and percentage presented? Without that the time savings seems relatively irrelevant?
- hajile 4y agoRelative percentages would be nice for more context, but saving ~12.5 seconds waiting on my build is significant enough that even if it were a small percentage of total time, it would still be well-worth doing. I think the real point here is that more profiling could give some great results without tons of work.
- HiJon89 4y agoThanks for the great writeup and the great work! The fact that half the comments here are dismissive or condescending is such a sad indictment of HN culture :(
- mhagemeister 4y agoAuthor here. Thanks for the kind words!
- mschuster91 4y agoThe old trap of tech debt. Basically... back when these tooling projects started, stuff like casting stuff around all the time, using un-optimized tree algorithms, inefficient value objects or whatever didn't matter - because the projects that were built using them were so small the inefficiencies didn't matter. And once the projects got bigger, inefficiencies weren't really looked at for three reasons: the obvious "if the source gets larger, the build time goes up as well", because people simply got more powerful hardware - and finally: Because most JS developers simply aren't your old guard neckbeard coders with decades of experience under their belt. They're fresh coding academy graduates that don't teach stuff like flame graphs or performance tracing in general, and simply are used to stuff taking a lot of time. And fwiw, knowledge of optimization techniques isn't really widespread even in senior developers. Modern computers are simply "fast enough" for many people (and especially corporate beancounters) to not care.
- hajile 4y agoThere's another issue at play here. Nobody makes money from making SVGO faster. Your customers don't run SVGO and only care about the performance of the output. Devs care, but not enough to do anything about it. Companies care about dev productivity, but the cost of the hours to run through the profiler isn't something they are willing to pay for. And finally, there aren't enough maintainers/contributors to these projects. The core team is some volunteers. When they have spare time for the project, it's almost always going to be used to keep up with web standards, add missing features, or chase down bugs that impact the final output. They know performance matters, but thers's always something more important to do instead.
- bouk 4y agoOne of the best bang for your buck optimizations I did recently was add the following to our next.config.js (we're using V13): const swcOptions = require('next/dist/build/swc/options') const { getLoaderSWCOptions } = swcOptions swcOptions.getLoaderSWCOptions = function (...args) { const result = getLoaderSWCOptions(...args) result.jsc.target = 'es2022' return result } This overrides the target https://swc.rs/ https://swc.rs/ uses from the default of es5 to es2022, which sped up our quite heavy app by up to 3X in hot code paths, just by using newer JS syntax instead of falling back to 'transpiled' versions. Unfortunately Next.JS doesn't allow setting the target directly. My hunch is that especially this syntax in our hot path was a big benefactor: function cloneAndSetProperty(obj, prop, value) { return { ...obj, [prop]: value } }
- chrismorgan 4y agoOn your cloneAndSetProperty, consider if you can use a prototype instead of a clone: function chainedPrototypePlusProperty(obj, prop, value) { const newObject = Object.create(obj); newObject[prop] = value; return newObject; // Alternatively: return Object.create(obj, { [prop]: { configurable: true, enumerable: true, value } }); } It depends entirely on what you’re doing with it (not mutating obj while you keep the returned object, not depending on properties being own), but in some cases this will be suitable, and depending on the data and what you’re doing, could be drastically faster (it’s O(1) rather than O(n) on the number of properties, and access after could be much of a muchness).
- bouk 4y agoI've tried this and found it to be much slower
- danfritz 4y agoYou can enable the experimental `browsersListForSwc: true,` option which uses your browserlist to determine the appropriate target. You can also disable `legacyBrowsers` ref: https://nextjs.org/blog/next-12-2#other-improvements https://nextjs.org/blog/next-12-2#other-improvements
- tonto 4y agoreminds me of a bug bounty type post :) would be nice to get awarded similarly
- newbieuser 4y agoMost javascript packages come with a memory leak as well as ease of use. and as the number of packets increases, unfortunately the memory leak increases exponentially.
- jgalt212 4y agoIf you're serving SVG over gzip or similar, you'd be hard-pressed to sell me on the value of SVG optimization. I'm also not keen on javascript minimization. Most of the delay on the client in not in the transport, but in the parsing. For this reason, I do think tree-shaking has good value.