That's the only image with a non-zero EXIF orientation. Which probably means you're using an older browser (e.g. Chrome started respecting EXIF orientation in version 81+, which I think came out 3 years ago?). You'd have to update your browser for it to display correctly.
HN user
constexpr
Hello! I made this. People are talking about not wanting pictures to be initially blurry before they finish loading. I understand that too, and I'm not sure how I feel about it myself (I could go either way).
But for what it's worth, I actually made this for another use case: I have a grid of images that I want to be able to zoom really far out. It'd be nice to show something better than the average color when you do this, but it would be too expensive to fetch a lot of really small images all at once. ThumbHash is my way of showing something more accurate than a solid color but without the performance cost of fetching an image. In this scenario you'd only ever see the ThumbHash. You would have to zoom back in to see the full image.
There are two performance implications of "modularization": initialization-time and run-time.
You are correct that initializing many modules is usually slower than initializing one module [1]. However, bundling puts all modules into one file, so this PR doesn't actually change anything here. Both before and after this PR, the TypeScript compiler will be published as a single file.
At run-time, switching to ES modules from another JavaScript module system can be a significant performance improvement because it removes the overhead of communicating between them. Other module systems (e.g. TypeScript namespaces, CommonJS modules) use dynamic property accesses to reference identifiers in other modules while ES modules use static binding to reference the identifiers in other modules directly. Dynamic property access can be a big performance penalty in a large code base. Here's an example of the performance improvement that switching to ES modules alone can bring: https://github.com/microsoft/TypeScript/issues/39247.
[1] This is almost always true. A random exception to this is that some buggy compilers have O(n^2) behavior with respect to the number of certain kinds of symbols in a scope, so having too many of those symbols in a single scope can get really slow (and thus splitting your code into separate modules may actually improve initialization time). This issue is most severe in old versions of JavaScriptCore: https://github.com/evanw/esbuild/issues/478. When bundling, esbuild deliberately modifies the code to avoid the JavaScript features that cause this behavior.
Carto was a blast and really well made. Loved the humor and world building, and the puzzles were satisfying.
I came across one of these once without knowing about the phenomenon. It’s in Golden Gate Park near the intersection of JFK Drive and Transverse Drive. Water in the stream next to the road appears to flow uphill. Very surreal.
No, I haven't experimented with generics and esbuild. I hadn't considered whether generics could improve performance or not. Just thinking about it quickly now. I'm not convinced it would because esbuild hardly makes use of interface{}. If someone can demonstrate a noticeable performance improvement then I'd be happy to start using generics for that reason.
The main pattern esbuild uses is an interface with a single dummy method to denote a union type like this: https://github.com/evanw/esbuild/blob/34899aaa1d76acd3b4adc5.... It's used several times and is basically esbuild's core data structure. I'd like to be able to optimize this pattern. Specifically I'd like to avoid each reference to a union type taking up two whole pointers in memory (Go represents interfaces as a pointer to the object and a separate pointer to the method table).
I'm only using the method table as a tag for the tagged union, so ideally it'd be a single byte or something even less expensive like part of the pointer. I don't think generics can help with this? But Go doesn't let you do fancy stuff like this so I'm just leaving it be. A low-level language like C++/Rust could do better here, but that comes at the cost of a significant decrease in productivity, so I'm ok with this trade-off.
That comment wasn't about Go exclusively. My point was that porting single-threaded code to a native language doesn't necessarily cause huge order-of-magnitude speed gains, especially if you plan on sticking close to the original structure of the code to make porting future changes easy, but that in this specific case a port sounds likely to realize significant gains (which is very exciting!).
Shared-memory parallelism is necessary but not sufficient. Ideally you'd be able to only make some small changes in a few spots to greatly increase parallelism. I'm hopeful that TypeScript's AST parsing is a good candidate since ASTs can typically be constructed independently from the rest of the program state. Lowering and printing ASTs is also likely parallelizable. It would also be great if it was also possible to parallelize parts of the type checker somehow, but that sounds harder. I had some ideas about that in another comment. Anyway I think this means that it's likely that a port of the TypeScript compiler could realistically lead to significant speedups and may even lead to extreme speedups.
As far as language choice, it sounds like due to maintenance reasons the decision space is narrowed to native, garbage collected languages similar in object model to TypeScript but with parallelism primitives. Some candidates could be at least Go, JVM-based languages, or .NET-based languages. I think those can all be ahead-of-time compiled to a static binary? Another consideration is how easy/compact/fast/cross-platform binaries are. Go makes this trivial but I haven't used JVM or .NET AOT builds myself so I don't know how they compare in e.g. executable size or startup overhead. One advantage of picking Go is that esbuild has already demonstrated that Go is an effective choice for this problem domain, which may mean less work/research/unknowns. A disadvantage of Go here might be that the object model is different than TypeScript vs. something like Java or C#. But all else being equal the choice comes down to personal preference.
Yes that allows for shared memory. But the TypeScript compiler is written in TypeScript, and you can't put JavaScript objects (such as the TypeScript AST) in a SharedArrayBuffer. So you'd have to port the TypeScript compiler to another language that can target multithreaded WASM to do this.
You could also try sending JS objects between workers instead of using SharedArrayBuffer. But that copies the memory, which can take a significant amount of time, and of course also excludes certain kinds of advanced parallelism that require shared memory (instead of copied memory). Another drawback of using the JS worker model like this is that having multiple JS VMs probably means fewer CPU cores available due to there being multiple independent garbage collectors. I observed an effect like this when I tried to parallelize esbuild's JavaScript plugins. Running them in separate workers seemed to cause the gains from parallelism to stop when there were half as many workers as CPUs. I'm guessing is because half of the CPUs are busy collecting garbage for the other half. Using a language like Go with real shared memory presumably makes it easier for fewer resources to be spent on garbage collection.
Yeah I was afraid of that. Ideally it would be possible to do type checking in parallel at the function level or something, but TypeScript allows you to omit the return type and then infers the return type from the function body which adds additional dependencies and makes that more complicated to parallelize. I wonder if it would still be possible though. Maybe just for functions where the return type and all argument types are present? Then the approach would be sort of outside-in where you type check the unparallelizable global stuff serially first and then type check the remaining isolated function body stuff in parallel afterward.
The problem is that you can't use shared-memory parallelism in TypeScript. If you could, then the most straightforward way of speeding up the TypeScript compiler would be to parallelize the compiler, not to port it.
I'm the author of esbuild. I think this is a very smart, practical choice and is one of the approaches that has the best chance of succeeding (the other being a semi-automated porting tool instead of a purely manual port). I'm very excited to see someone take this approach. This is how I would do it if I were to do it. Web developers around the world are hoping this works out :)
The hard part about this project isn't doing the port but keeping up with the constant changes from the TypeScript team, especially if the TypeScript team isn't invested in helping you out. Staying as close as possible to the source language like this will be a big win there.
One of the big benefits of a native language like Go aside from lack of a JIT is the ability to easily add shared-memory parallelism. I haven't studied the TypeScript compiler internals so I'm not sure how easy it is to parallelize their type checker. However, I assume that it would be relatively straightforward to parallelize the parsing of JavaScript files, so I'd hope for at least be a big speedup there.
I'm the author of esbuild. I hadn't written Go before I started esbuild and I thought I would miss generics more, but I didn't. There is literally only one situation in the whole esbuild code base where I've found myself wishing for generics: sorting an array. The workaround is easy and involves implementing an interface with three methods, each of which is typically a single line: https://pkg.go.dev/sort#Interface. And I only needed a few of these interfaces for a few different types. That wasn't nearly enough of a reason to not use Go, especially considering the other advantages.
It is also all custom: https://github.com/esbuild/esbuild.github.io. YAML files that get converted to plain static HTML pages. It's designed to work well without any JavaScript. Even the animated graphs on the home page are done without JavaScript.
I'm the author of esbuild. Rest assured that I am very interested in supporting it and am actively working on it. It's just unfortunately very complicated to correctly, especially without affecting any of the other existing features. There are some recent updates at the bottom of this thread: https://github.com/evanw/esbuild/issues/253.
Both would be good to file. I’m happy to investigate myself if you don’t have the time.
One thing to be aware of is that some other tools that integrate esbuild do so incorrectly. For example, the 3rd-party integration of esbuild into Webpack mis-configures esbuild in a way that causes issues with JSX: https://github.com/privatenumber/esbuild-loader/pull/107. This isn’t a problem with esbuild itself.
Ah I see, no worries. I assumed you were trying to compare against a production build since that's what "parcel build" does.
I noticed you changed this to "dev" instead of "production". Just letting you know that this will select the development build of React instead of the production build, which is not optimized for size and will lead to a bigger output file.
Good luck with your project!
It would be great if you could file these as bugs against esbuild! Sounds like these should be easy to fix.
It's strange that they are comparing output size without passing --minify to esbuild. Why compare the size of the debuggable output?
And if you care about output size, you would also need to pass --define:process.env.NODE_ENV=\"production\" to esbuild since the benchmarks include React, and you don't want to bundle both the debug and release versions of React at the same time.
I think the odds are pretty good. The TypeScript compiler's performance is not optimal: https://twitter.com/evanwallace/status/1275945317045657602. Microsoft's TypeScript team has a lot of priorities and other things likely take precedence over performance work.
Also it's pretty trivial to build a faster compiler if you don't have to worry about type checking. You can still have type checking with a bundler that ignores types by running the official TypeScript type checker in parallel with your bundler.
A new JavaScript/TypeScript bundler written in Go that's 10-100x faster than Webpack, Rollup, and Parcel:
Shaking up the JavaScript build tool ecosystem with https://github.com/evanw/esbuild, a bundler and minifier that's 10x-100x faster than industry-standard ones (Webpack/Rollup/Parcel).
I'm not sure that this is a CPU bound workload. You're right that if this is CPU bound, LLVM's code generation should come out on top, even if only slightly, but that's not the case. Perhaps writing a JavaScript bundler is more of a memory bound or I/O bound task than a CPU bound task?
Yeah benchmarks are tricky. I'd rather not tweak the parameters for Rollup in this case because then it wouldn't be a fair comparison. Rollup would then be cheating because it has to do less work.
It could definitely be useful to have additional benchmarks that test other configurations though. For example, development build speed is also relevant and would make a good benchmark. I'll think about adding more benchmarks if I have time.
I also want a benchmark that tests a real app. I just haven't yet found a large-enough open source app to use as a benchmark. Do you know of one? I work at Figma and Figma's main app makes a great test case, but obviously using that as benchmark wouldn't be reproducible by others outside of Figma. Figma's code base is around the same size as the benchmark I'm using (so 10x the size of three.js).
Ultimately I'm planning on implementing a TypeScript parser in esbuild, at which point I'd like to switch to a benchmark of a TypeScript code base. I also want to include multiple entry points in the benchmark because that's something Figma's main app uses, and esbuild has specific optimizations for that case.
Long story short the benchmark I'm currently using was helpful for me during the initial development but it's not the final one I plan to use, and I will likely completely change it as the project evolves.
Edit: Oh yeah and gzip sizes are great to track. I've tracked these for previous compiler projects I've done and you're right, sometimes you need to make the output larger to make the gzip output smaller. I haven't looked at the gzip sizes at all yet and there is room for improvement. Check out https://github.com/evanw/esbuild/issues/6#issuecomment-58667... for an idea of what specific numbers might currently look like for esbuild compared to other minifiers.
Both the Rust and Go parsers were written by hand. They are also very similar (basically the Go version was a direct port of the Rust version) so the performance should be very comparable.
I assume by zero-copy you mean that identifiers in the AST are slices of the input file instead of copies? I was also careful to do this in both the Go and Rust versions. It's somewhat complicated because some JavaScript identifiers can technically have escape sequences (e.g. "\u0061bc" is the identifier "abc"), which require dynamic memory allocation anyway. See "allocatedNames" in the current parser for how this is handled.
Note that strings aren't slices of the input file because JavaScript strings are UTF-16, not UTF-8, and can have unpaired surrogates. So I represent string contents as arrays of 16-bit integers instead of 8-bit slices (in both Go and Rust).
In the past I tried using WTF-8 encoding (https://simonsapin.github.io/wtf-8/) for string contents, since that can both represent slices of the input file while also handling unpaired surrogates, but I ended up removing it because it complicated certain optimizations. I think the main issue was having to reason through weird edge cases such as constant folding of string addition when two unpaired surrogates are joined together. I think it's still possible to do this but I'm not sure how much of a win it is.
Thanks! Will do. No wonder the three.js repo takes so long to download :)
You probably aren't seeing the AST optimizations because they are actually in the parser. Search for "mangleSyntax" to find the optimization code.
This would normally be done in a separate pass but I'm trying to minimize the number of passes done over the full AST for speed. I figured it'd be faster to optimize the AST as it's parsed because the newly-created AST nodes are likely to still be in the CPU cache. That said, I haven't tested the performance impact of doing this. I just did this based on intuitions from performance work on previous compiler projects.
If you're doing arena allocation in a compiler you might as well just leak all your allocations (which you could get with bumpalo with a 'static lifetime); then you won't have to deal with lifetimes at all.
I considered this but it's a very limiting hack. Ideally esbuild could be run in watch mode to do incremental builds where only the changed files are rebuilt and most of the previous compilation is reused. Memory leaks aren't an acceptable workaround to memory allocation issues in that case. While I don't have a watch mode yet, all of esbuild was architected with incremental builds in mind and the fact that Go has a GC makes this very easy.
500-line functions are hard to read.
I totally recognize that this is completely subjective, but I've written a lot of compiler code and I actually find that co-locating related branches together is easier for me to work with than separating the contents of a branch far away from the branch itself, at least in the AST pattern-matching context.
It's hard for me to go back to a language without pattern matching and enums
I also really like these features of Rust, and miss them when I'm using other languages without them. However, if you look at the way I've used Go in esbuild, interfaces and switch statements give you a way to implement enums and pattern matching that has been surprisingly ergonomic for me.
The biggest thing I miss in Go from Rust is actually the lack of immutability in the type system. To support incremental builds, each build must not mutate the data structures that live across builds. There's currently no way to have the Go compiler enforce this. I just have to be careful. In my case I think it's not enough of a problem to offset the other benefits of Go, but it's definitely not a trade-off everyone would be comfortable making.
Yes this was a surprise for me too. The benchmark I used for esbuild deliberately tests full build time without a cache, since that is an important metric to me. I assume Parcel is talking about incremental build times when the cache has already been populated. In that case I'm sure Parcel is not as slow.
Have you tried using jemalloc? It can help a lot for situations like this.
Huh, I thought Rust already used jemalloc by default. I looked it up and it looks like it was removed relatively recently. When I was doing this experiment, I was using a version of Rust that included jemalloc by default so that 10% number already uses jemalloc. I remember this because I also thought of trying to speed up the allocator.
Like I said above, I profiled both Go and Rust and the 10% slowdown with Rust appeared to be running destructors for the AST. I think the appropriate solution to this would be some form of arena allocator instead of changing the system allocator. But that gets even more complicated with lifetimes and stuff.
Can you work around this by using separate functions for the branches?
Yeah, I could have tried restructuring my code to try to avoid compiler issues. But this would have been even more time spent working around issues with Rust. Go was better than Rust by pretty much every metric that mattered for me, so I went with Go instead.
It's too bad because I was initially super excited about the promise of Rust. Being able to avoid the overhead of GC while keeping memory safety and performance is really appealing. But Rust turned out to be not a productive enough language for me.
It's cool to wake up in the morning and see my project on HN! For the record, I'm not trying to compete with the entire JavaScript ecosystem and create an extremely flexible build system that can build anything.
I'm trying to create a build tool that a) works well for a given sweet spot of use cases (bundling JavaScript, TypeScript, and maybe CSS) and b) resets the expectations of the community for what it means for a JavaScript build tool to be fast. Our current tools are way to slow in my opinion. Let's build faster tools!