Speeding up the JavaScript ecosystem - Semver

1 Share

Whilst dabbling with the Preact repo I noticed that running npm install takes more than 3s. This seems excessively long and piqued my interest. I was waiting for a good use case to try out cpupro anyway, so this seemed like a perfect fit.

Cpuprof shows how much CPU team each package consumed. The semver package alone consumes 600ms of the 3s total time.

The tar library is kinda expected to be up there as a decent amount of time is spent extracting tarballs. What captured my attention was the semver package though. It's the package that compares all version ranges of all the dependencies in your project to figure out which versions to install. The versioning is based on the semver standard which defines a major, minor, patch and an optional prerelease component:

1.2.3 # major.minor.patch
2.0.0-alpha.3 # major.minor.patch-prerelease

Package managers go beyond that to allow you to specify ranges or constraints. Instead of only allowing exact versions, they allow you to pass something like ^1.0.0 which means: "Allow any minor or patch version bigger than this version. Ranges can also be composed. You'll often find this sort of format when declaring peerDepdendencies: 1.x || 2.x. Npm supports a plethora of ways to express desired version ranges for your dependencies.

1.2.3  # =1.2.3
4.0.0-alpha.4 # =4.0.0-alpha.4
4.0.0-2 # =4.0.0-2

# Partial versions are also possible
1 # =1.0.0
1.2 # =1.2.0

# Ranges
^2.0.0 # >= 2.0.0 && <3.0.0
~2.0.0 # >= 2.0.0 && <2.1.0
1.x || 2.x # >=1.0.0 || >=2.0.0
1.2.3 - 1.4.0 # >=1.2.3 && <=1.4.0

1.x # >=1.0.0

# ...and more

Parse, don't validate

To get some idea how often the npm binary calls into semver, I've cloned the npm repository, ran npm install and wrapped all exports of the semver library with functions that log out which function was called and with what arguments.

Doing an installation in the preact repo calls about 21.2k times in total into the semver. That's way more than I would've expected. Here are some stats of the functions called:

Function Callcount
satisfies 11151
valid 4878
validRange 4911
sort 156
rcompare 79
simplifyRange 24

Looking at the table we can see that the call count is entirely dominated by validating and checking if version constraints are satisfied. And that makes sense, because that's what a package manager is supposed to do during installation of dependencies. Looking at the raw function call logs we can see a common pattern:

valid [ '^6.0.0', true ]
validRange [ '^6.0.0', true ]
satisfies [ '6.0.0', '^6.0.0', true ]
valid [ '^0.5.0', true ]
validRange [ '^0.5.0', true ]
satisfies [ '0.5.0', '^0.5.0', true ]
valid [ '^7.26.0', true ]
validRange [ '^7.26.0', true ]
satisfies [ '7.26.0', '^7.26.0', true ]
valid [ '^7.25.9', true ]
validRange [ '^7.25.9', true ]
satisfies [ '7.25.9', '^7.25.9', true ]
valid [ '^7.25.9', true ]
validRange [ '^7.25.9', true ]
satisfies [ '7.25.9', '^7.25.9', true ]
valid [ '^7.26.0', true ]
validRange [ '^7.26.0', true ]
satisfies [ '7.26.0', '^7.26.0', true ]

A huge portion of the satisfies calls are preceded by a call to both valid and validRange. This hints at us that these two functions are used to validate the arguments passed to satisfies. Since the source of the npm cli is public we can check the source code for what these functions are doing.

const valid = (version, options) => {
const v = parse(version, options);
return v ? v.version : null;
};

const validRange = (range, options) => {
try {
// Return '*' instead of '' so that truthiness works.
// This will throw if it's invalid anyway
return new Range(range, options).range || "*";
} catch (er) {
return null;
}
};

const satisfies = (version, range, options) => {
try {
range = new Range(range, options);
} catch (er) {
return false;
}
return range.test(version);
};

To validate input correctness, both validator functions parse the input data, allocate the resulting data structure and return back the original input string if it passed the check. When we call the satisfies function, we do the same work that we just threw away again. This is a classic case of doing twice the work necessary due to not following the "Parse, don't validate" rule.

Everytime you're dealing with parsers and you're validating the inputs before doing the parsing you're burning unnecessary CPU cycles. Parsing itself is a form of validation. It's pointless. By getting rid of the duplicate validation we can save about 9.8k function calls.

With us having logged the function calls prior, we have an ideal benchmark case and can measure the timings with and without duplicate validation. For that I'm using the deno bench tool.

benchmark time/iter (avg) iter/s (min … max) p75 p99 p995
validate + parse 99.9 ms 10.0 ( 85.5 ms … 177.3 ms) 91.1 ms 177.3 ms 177.3 ms
parse 28.0 ms 35.7 ( 22.8 ms … 73.3 ms) 26.4 ms 73.3 ms 73.3 ms

Removing the duplicate validation gives us a nice speedboost already. It makes the code ~70% faster.

Does slapping a cache in front of things help?

Over time the folks working on the semver library realized this problem and added an LRU cache, which is sorta a special variant of a Map in JavaScript terms that has a limited number of entries. This is done to avoid the Map growing indefinitely in size. And it turns out it's somewhat effective at speeding things up. Logging out whether a cache entry was found or missed reveals that only 523 times out of 26k calls returned a non-cached result. That's pretty good!

If we remove the caching we can get an idea of how much time it saved.

With the LRU cache removed the semver package consumes 731ms

In total, adding the cache saved about 133ms. Which I guess is not nothing, but far from being effective in the grand picture, despite the amount of cache hits.

Scenario Time
Uncached 731ms
Cached 598ms

Caches are a bit of a pet peeve of mine. They can be incredibly useful if used sparingly at the right places, but they are notoriously overused across the JavaScript ecosystem. It often feels like a lazy way to increase performance, where instead of making your code fast, you try to cache the results and kinda hope for the best. Making your code fast instead, yields much better results in many cases.

Caches should be seen as the last form of measure to take when all other options have been exhausted.

How fast can we make it?

With semver having a small grammar to parse, I kinda felt nerdsniped to have a go at it myself. Over the course of a day I wrote a semver parser and added support for all the ways npm allows you to specify a semver constraint. The code is doing nothing out of the ordinary. It loops over the input string, detects the relevant tokens and creates the final data structures out of that. Anyone familiar with parsers would've come up with the same code. It's doing nothing special. No secret JavaScript performance tricks or anything.

function parseSemver(input: string): Semver {
let ctx = { input, i: 0 };
let major = 0;

let ch = ctx.input.charCodeAt(ctx.i);
if (ch >= Char.n0 || ch <= Char.n9) {
major = parseNumber(ctx);
} else {
throw invalidErr(ctx);
}

ch = expectDot(ctx);

let minor = 0;
if (ch >= Char.n0 || ch <= Char.n9) {
minor = parseNumber(ctx);
} else {
throw invalidErr(ctx);
}

// ...and so on
}

And then we need some code that can check if a version satisfies semver constraints, which is structured in a similar way.

With us having logged all function calls prior, we have the ideal dataset for a benchmark. For the first one, although I've kept the validate + parse steps.

benchmark time/iter (avg) iter/s (min … max) p75 p99 p995
node-semver 99.9 ms 10.0 ( 85.5 ms … 177.3 ms) 91.1 ms 177.3 ms 177.3 ms
custom 3.9 ms 255.3 ( 3.7 ms … 6.1 ms) 4.0 ms 4.7 ms 6.1 ms

Even with doing the unnecessary validate step, the custom parser is 25x faster. If we capture a CPU profile of the amount of work done, the difference is even more stark.

CPU profile of node's semver library shows many function calls. CPU profile of the custom parser contains much fewer function calls and is less noisier.

Let's get rid of the unnecessary validation step for the next benchmark for both of them.

benchmark time/iter (avg) iter/s (min … max) p75 p99 p995
node-semver 28.0 ms 35.7 ( 22.8 ms … 73.3 ms) 26.4 ms 73.3 ms 73.3 ms
custom 2.9 ms 347.2 ( 2.7 ms … 6.4 ms) 2.9 ms 3.4 ms 6.4 ms

With a level playing field of not doing unnecessary validation the difference is much smaller, but still in the order of 10x faster. So in summary the semver checks can be made 33x faster than they are now.

Conclusion

As usual, the tools in the JavaScript ecosystem aren't slow because of the language, but because of slow code. At the time of this writing, the semver library is used in all popular package managers. It's used in npm, yarn and pnpm. Making this faster or switching to a faster alternative would speed the installation process up considerably for all of them.

Read the whole story
jlvanderzwan
1 day ago
reply
ttencate
4 hours ago
Now I wonder if a regex could parse it, and if that would be even faster. Regex engines are hyper-optimized already.
Share this story
Delete

Saturday Morning Breakfast Cereal - Unified

2 Shares


Click here to go see the bonus panel!

Hovertext:
Btw, the real life Dr. Whiteson, who podcasts with my wife, has a new book out called Do Aliens Speak Physics? which you should go check.


Today's News:
Read the whole story
jlvanderzwan
1 day ago
reply
Share this story
Delete

WHEEL SMASHING LORD 5-148

1 Comment

“Guild Ship Light of Profit: 138 guns, 22 decks, 1050 personnel. Average wage of crewman: 30 marks an hour. Average cost of Arten shell: 230 thousand marks. Demons per liter: 45 marks. Cost per minute of operation: 1.2 million marks. […] ↓ Read the rest of this entry...
Read the whole story
jlvanderzwan
5 days ago
reply
I have a hunch this might take close to six billion off-screened demons to finish
Share this story
Delete

Conway’s pinwheel tiling

1 Comment

John Conway discovered a right triangle that can be partitioned into five similar triangles. The sides are in proportion 1 : 2 : √5.

You can make a larger similar triangle by making the entire triangle the central (green) triangle of a new triangle.

Here’s the same image with the small triangles filled in as in the original.

Repeating this process creates an aperiodic tiling of the plane.

The tiling was discovered by Conway, but Charles Radin was the first two describe it in a publication [1]. Radin attributes the tiling to Conway.

Related posts

[1] Charles Radin. “The Pinwheel Tilings of the Plane.” Annals of Mathematics, vol. 139, no. 3, 1994, pp. 661–702.

The post Conway’s pinwheel tiling first appeared on John D. Cook.
Read the whole story
jlvanderzwan
6 days ago
reply
Abdalla G. M. Ahmed has done some really cool things with pinwheel tiles:

https://abdallagafar.com/publications/pinwheel-curve/
Share this story
Delete

‘Ladybugs of the Sea’ Take Top Honors in the 2025 Ocean Photographer of the Year Contest

1 Share
‘Ladybugs of the Sea’ Take Top Honors in the 2025 Ocean Photographer of the Year Contest

From tiny creatures measuring only three millimeters to humpback whales of incredible proportions, the animals and seascapes represented in this year’s Ocean Photographer of the Year explore the incredible diversity and fragility of our cherished saltwater ecosystems.

More than 15,000 images were submitted to the 2025 contest from around the globe. Penguins off the coast of Antarctica captured by Romain Barats are complemented by stunning coral reefs shot by divers like Kim Hyeon Min and Jenny Stock. The contest is co-organized by Oceanographic Magazine and Blancpain.

a large coral in a reef
Kim Hyeon Min. Countless juvenile fish swirl around a dome-shaped coral, Indonesia

This year’s top prize goes to Yury Ivanov for his wonderful depiction of two tiny amphipods, which, despite their minuscule size, display beautiful colors and symmetry. “It required a lot of patience and precision to compose and light the shot properly,” Ivanov says. “The result reveals an intimate glimpse of underwater life that is often overlooked.”

Explore the winners’ gallery on the competition’s website.

penguins swim underwater
Romain Barats. Gentoo penguins resembling rockets dart through the water, Antarctica
Richard Smith. A dwarf seahorse hides among green algae, U.S.
a tiny jellyfish-like creature deep in the ocean
Jialing Cai. Almost ethereal in its translucency, a juvenile wunderpus octopus is surrounded by a variety of small zooplankton, such as larval shrimps, crabs, and worms, Anilao, Philippines
a goby cradles its eggs between two sea squirts
Giancarlo Mazarese. Between two sea squirts, a goby cradles its eggs. Bali, Indonesia
three sharks swim close together
Brooke Pyke. A mating ritual between three tawny nurse sharks. Baa Atoll, Maldives
a coral reef with a huge school of fish swimming nearby
Jenny Stock. A healthy reef in Raja Ampat, Indonesia
a whale swims with rope around its body
Claudio Moreno Madrid. An entangled humpback whale off Ningaloo Reef, Australia
an aerial view showing people gathered on a beach with buckets and rafts of fish
Natnattcha Chaturapitamorn. The morning ritual at Tam Tien beach, reflecting a deep connection between the sea and coastal livelihoods, Vietnam
a diver is very small underwater beside two whales
Alvaro Herrero. A freediver, accompanied by two humpback whales, maintains a respectful distance, French Polynesia
a person standing on a boat amid a watery aquaculture area
Shi Xioawen. Laver cultivation poles intertwine with fishermen labouring at work, China

Do stories and artists like this matter to you? Become a Colossal Member today and support independent arts publishing for as little as $7 per month. The article ‘Ladybugs of the Sea’ Take Top Honors in the 2025 Ocean Photographer of the Year Contest appeared first on Colossal.

Read the whole story
jlvanderzwan
7 days ago
reply
Share this story
Delete

Glimpse Spectacularly Tiny Worlds in Winning Videos from Nikon’s Small World In Motion Competition

1 Share
Glimpse Spectacularly Tiny Worlds in Winning Videos from Nikon’s Small World In Motion Competition

From a remarkable demonstration of flower self-pollination to algae swimming in a water droplet in a Japanese 50 Yen coin, the winners of this year’s Nikon Small World in Motion competition capture some of the natural world’s most beautiful, otherworldly, and otherwise invisible phenomena.

The top prize was awarded to Michigan-based photographer Jay McClellan, who captured a timelapse of a thymeleaf speedwell flower, incorporating image stacking techniques to depict the blossom at 5x magnification. McClellan’s video of crystallizing cobalt, copper, and sodium chlorides was awarded an honorable mention, too.

1st Place: Jay McClellan (U.S.). Self-pollination in a flower of thymeleaf speedwell (Veronica serpyllifolia)

Biology takes center stage in the Small World competition, where images are captured through a variety of means to zoom in on things we may not be able to see with the naked eye. Researchers and enthusiasts from all over the world submitted dazzling views of slime mold, mycelium, cellular reproduction, sensory neurons, and more.

São Paolo-based scientist Dr. Alvaro Migotto documented a microscopic marine mollusk larva in the process of metamorphosis, for example. Penny Fenton recorded a tardigrade crawling around on an algae colony. And Benedikt Pleyer, based in Germany, captured dozens of cyanobacteria filaments using polarized light.

Overall, judges selected five top winners, plus 19 honorable mentions. See all videos in the winners’ gallery on the contest’s website. And keep an eye out for winners of the Nikon Small World photo competition, which will be announced on October 15.

Honorable mention: Wim van Egmond (The Netherlands). Hat thrower fungus (Pilobolus) on rabbit dung
a screenshot from a video of a male dung beetle (Sulcophanaeus imperator) composed of 7,073 individual images
Honorable mention: Janosch Waldkircher (Switzerland). Male dung beetle (Sulcophanaeus imperator), excerpted from a video composed of 7,073 individual images
2nd Place: Benedikt Pleyer (Germany). Volvox algae swimming in water drop that has been pipetted into the central opening of a Japanese 50 Yen Coin
Honorable mention: Jay McClellan (U.S.). Dissolution and crystallization of cobalt, copper, and sodium chlorides
Honorable mention: Dr. Maik C. Bischoff (U.S.). Developing testis of a fly showing actin cytoskeleton (teal) and nuclei (red)
Honorable mention: Dr. Alvaro Migotto (Brazil). Marine mollusk larva before and after metamorphosis
3rd Place: Dr. Eric Vitriol (U.S.). Actin and mitochondria in mouse brain tumor cells

Do stories and artists like this matter to you? Become a Colossal Member today and support independent arts publishing for as little as $7 per month. The article Glimpse Spectacularly Tiny Worlds in Winning Videos from Nikon’s Small World In Motion Competition appeared first on Colossal.

Read the whole story
jlvanderzwan
7 days ago
reply
Share this story
Delete
Next Page of Stories