Repository navigation
Performance of WHATWG URL API #30334
Description
-
Version:
- 10.17.0
- 12.13.0
- 13.1.0
-
Subsystem:
url
I have encountered performance regression when mitigating deprecated Legacy URL API to newer recommended WHATWG URL API for a performance-sensitive application. I have set up a benchmark: https://runkit.com/sukkaw/5dc5924f1ba0d60013d87cb3
const Benchmark = require('benchmark');
const Suite = new Benchmark.Suite;
// const { URL } = require('url') is only needed for Nodejs 8.
const { parse, URL } = require('url');
const safeWhatwgUrl = (str) => {
// Handle relative path
try {
return new URL(str);
} catch (e) {
return str;
}
};
Suite.add('Absolute URL - Legacy URL parse()', () => {
parse('https://skk.moe/path/to/something');
}).add('Absolute URL - WHATWG URL API', () => {
new URL('https://skk.moe/path/to/something');
}).add('Absolute URL - safe WHATWG URL', () => {
// Should be very slow, since try...catch is very expensive
safeWhatwgUrl('https://skk.moe/path/to/something');
}).add('Relative Path - Legacy URL parse()', () => {
parse('/path/to/something');
}).add('Relative Path - WHATWG URL API', () => {
// pass a `base` to handle relative path
new URL('/path/to/something', 'https://skk.moe/');
}).add('Relative Path - safe WHATWG URL', () => {
// Should be even slower.
safeWhatwgUrl('/path/to/something');
}).on('cycle', function(event) {
console.info(String(event.target));
}).run();Resulted in:
Node.js 8.16.2
"Absolute URL - Legacy URL parse() x 214,035 ops/sec ±1.94% (84 runs sampled)"
"Absolute URL - WHATWG URL API x 182,604 ops/sec ±0.62% (88 runs sampled)"
"Absolute URL - safe WHATWG URL x 162,791 ops/sec ±3.63% (83 runs sampled)"
"Relative Path - Legacy URL parse() x 1,008,555 ops/sec ±3.55% (74 runs sampled)"
"Relative Path - WHATWG URL API x 70,292 ops/sec ±2.69% (80 runs sampled)"
"Relative Path - safe WHATWG URL x 55,500 ops/sec ±2.62% (85 runs sampled)"
On Node.js 8.16.2, The WHATWG URL API is 15% slower than Legacy URL API when handle a normal URL. It seems normal and acceptable.
Node.js 10.17.0
"Absolute URL - Legacy URL parse() x 483,508 ops/sec ±1.00% (93 runs sampled)"
"Absolute URL - WHATWG URL API x 286,201 ops/sec ±1.11% (93 runs sampled)"
"Absolute URL - safe WHATWG URL x 272,538 ops/sec ±1.00% (93 runs sampled)"
"Relative Path - Legacy URL parse() x 2,127,003 ops/sec ±0.46% (95 runs sampled)"
"Relative Path - WHATWG URL API x 169,296 ops/sec ±0.58% (96 runs sampled)"
"Relative Path - safe WHATWG URL x 96,357 ops/sec ±0.11% (98 runs sampled)"
On Node.js 10.17.0, The WHATWG URL API is 40% slower than Legacy URL API when handle a normal URL.
Node.js 12.13.0
"Absolute URL - Legacy URL parse() x 393,703 ops/sec ±1.21% (89 runs sampled)"
"Absolute URL - WHATWG URL API x 225,574 ops/sec ±0.59% (92 runs sampled)"
"Absolute URL - safe WHATWG URL x 208,342 ops/sec ±0.98% (90 runs sampled)"
"Relative Path - Legacy URL parse() x 1,513,601 ops/sec ±3.66% (84 runs sampled)"
"Relative Path - WHATWG URL API x 114,788 ops/sec ±1.29% (92 runs sampled)"
"Relative Path - safe WHATWG URL x 10,425 ops/sec ±0.99% (89 runs sampled)"
On Node.js 12.13.0, The WHATWG URL API is 42% slower than Legacy URL API when handle a normal URL.
Node.js 13.1.0
"Absolute URL - Legacy URL parse() x 345,345 ops/sec ±0.94% (87 runs sampled)"
"Absolute URL - WHATWG URL API x 212,058 ops/sec ±0.41% (91 runs sampled)"
"Absolute URL - safe WHATWG URL x 200,212 ops/sec ±0.76% (92 runs sampled)"
"Relative Path - Legacy URL parse() x 1,407,585 ops/sec ±1.72% (89 runs sampled)"
"Relative Path - WHATWG URL API x 108,171 ops/sec ±0.51% (90 runs sampled)"
"Relative Path - safe WHATWG URL x 10,279 ops/sec ±0.57% (90 runs sampled)"
On Node.js 13.1.0, The WHATWG URL API is 38% slower than Legacy URL API when handle a normal URL.
And when I try to pass a base to handle relative path, it becomes even more slower. I believe it is because I pass a string to base so The WHATWG URL have to parse base first.
Activity
joyeecheung commentedon Nov 10, 2019
The last time I looked into the performance of the WHATWG URL parsing the bottleneck lies in the usage of
std::stringin the C++ layer (potentially fixable with something like zones, but that might be a bit over-engineering...).I wonder what the performance drop from v8.x to v10.x could be from though..
SukkaW commentedon Nov 22, 2019
benchmark run on
repl.it(Node.js 10.16.0): https://repl.it/repls/PromotedLooseDemosAnd still, Legacy URL API is way faster (at least 2x) than The WHATWG URL API.
Fix URL parsing w/ trailing slash & querystring (#1539)
Prinzhorn commentedon Feb 15, 2020
I'm seeing
url.parsebeing roughly 15 times faster for "garbage" / "could be URL" (I'm throwing large amounts of data at it, extracting stuff I need in a fuzzy fashion). I know it's an odd use-case, but it's a use-case.Reproducible here https://repl.it/repls/LimpingQuarrelsomeCoins
SukkaW commentedon Feb 15, 2020
@Prinzhorn
try { } catch (e) { }should be considered as very slow (which I have presented in my benchmark) and should be avoided.devsnek commentedon Feb 15, 2020
@SukkaW it was slower in your benchmarks for two reasons: 1) you had an extra function call, and 2) the second one always threw, and throwing is a slower path than not throwing. I would argue it shouldn't have been throwing anyway because you should have passed a base to it like you did the other relative test.
SukkaW commentedon Feb 15, 2020
@devsnek
As you can see, the WHATWG URL API just can not handle any relative path. There are always cases that no
basecould be given. And the code snippet I described below are widely used:And even a
baseis given (no matter if there is atry..catchblock) when parsing a relative path, the WHATWG URL is still a kind of 3x slower than the Legacy URL API.devsnek commentedon Feb 15, 2020
@SukkaW right... I agree that the whatwg parser is slower. My point was that your comparison was not really apples to apples (you were comparing a parse that always passed to a parse that always failed), and that try/catch isn't really slow.
SukkaW commentedon Feb 15, 2020
@devsnek we both agree that
try catchcould be as fast as code without them when there is no exception thrown (which is also presented in the first part of my benchmark).However in the specific case related to this issue, people are using
try catchto handle relative path directly (as I mentioned code fromeggjs, nobaseis given).So in the second part of my benchmark I setup a case when only relative path to be parsed (and
try catchfor no givenbase).Adding a case that has
basepassed is only to show that even with no exception to be catched bytry catch, implementations with the WHATWG URL API is still not fast enough. This is what I thought when setting up my benchmark.Prinzhorn commentedon Feb 16, 2020
@SukkaW thanks, I didn't know try/catch was that slow. However, once
url.parsewill disappear we have to use try/catch in order to safely replace it with the new API. So you cannot have a real world comparison without try/catch, it is a part of the new API if you will. But I think we already agree on that 👍jasnell commentedon Feb 17, 2020
Unfortunately there's not a lot we can do here. The existing
url.parse()uses a number of performance optimization tricks that make it faster but have had the side effect of making it largely unmaintainable. It has a number of security and parsing bugs that cannot be fixed properly without severe impact on it's performance and backwards compatibility. The WHATWG URL parser can, I'm sure, be made faster but a significant reason it is slower is because it takes a much stricter approach to spec compliance and favors parsing correctness over performance.SukkaW commentedon Feb 17, 2020
@jasnell
According to my benchmark, the WHATWG URL API is only 15% slower than the Legacy URL API for Node.js 8. But for Node.js 10 it becomes nearly 40% slower. There is space for performance improvement, right?
jasnell commentedon Feb 17, 2020
For sure yes! Just need to make certain we do not sacrifice spec compliance at all
himself65 commentedon Apr 29, 2020
watilde commentedon Sep 22, 2020
I'm also sharing benchmark results that I ran. It's fair to say that legacy URL is faster than WHATWG's URL as legacy one has less processing and the newer version of WHATWG's URL should have more logics within WHATWG's URL itself just because some updates in the module are
major-updateso that it should be slower. However, a few cases have more than about 5% deteriorate. It seems to be worth tracking for a while to figure out why this happens.Refs:
v12.18.3 vs v14.11.0
v12.18.3 vs master
v14.11.0 vs master
SukkaW commentedon Feb 6, 2023
@anonrig Thanks for your hard work! I was not expecting this issue to get fixed after years.