-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Faster SSR #5701
Faster SSR #5701
Conversation
The build's currently failing - it looks like because of a typescript type mismatch. |
The argument is server side, but is there a reason to use numbers instead of booleans anywhere at all ? I doubt the difference of one single easily gzip-able ascii character from The cost is so tiny it shouldn't warrant an argument, yet it also impedes on readability in the codebase, hence my question |
src/runtime/internal/ssr.ts
Outdated
|
||
while (pattern.test(html)) { | ||
const i = pattern.lastIndex - 1; | ||
escaped += html.slice(last, i) + escapes[html[i]]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for what it's worth, replacing escapes[html[i]]
with const ch = html[i]
and then (ch === '&' ? ch === '"' ? '"' : '&' : '<')
seems to shave another 10% off the execution time when I test it.
but i'm surprised the regexp performs so badly; interestingly enough, it's the function as second parameter that's the slowdown. html.replace(/&/g, '&').replace(/</g, '<')
is pretty much exactly the same speed as the new code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow. Maybe we should just do that instead then? How are you getting those numbers, and can you share them?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just took a 500 kb HTML document and ran
const html = readFileSync('bigdocument.html').toString();
const start = performance.now();
for (let i = 0; i < 2000; i++) {
escape(html);
}
console.log(performance.now() - start, 'ms');
Our documents of course tend to be smaller so there is a risk this doesn't correspond to real-life performance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The .replace.replace
version runs in 3.5s, the code in the PR with escapes[html[i]]
in 3.6s, if you replace it by the ternary operator in about 3.2s, and the original code with a function as second parameter to replace in 14.5s.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my testing a switch
on character code (charCodeAt
) is WAY faster than a simple object lookup. Don't ask me why. IE: (from another implementation)
while (match = matchHtmlRegExp.exec(str)) {
switch (str.charCodeAt(match.index)) {
case 34: // "
escape = '"'
break
I forget but it was something like 240 ops/sec vs 280 ops/sec (on a large file). I just submitted a 30% speed improvement patch to replace-html
library.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(ch === '&' ? ch === '"' ? '"' : '&' : '<')
I believe this is broken and it needs to be (ch === '&' ? '&' : (ch === '"' ? '"' : '<'))
. It does appear faster than the original version in this PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The switch
method is slower than both the ternary and the original PR implementation in my testing with @ehrencrona's benchmark above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The switch method is slower than both the ternary and the original PR implementation in my testing
Another thing we have to keep in mind here is that almost 2 years have passed. It's very hard IMHO to micro-tune JS performance (over the long-term). Engines vary, browsers vary, things change with time. The thing that was fastest 2 years ago might bench worse today. The regex engines used in the major browsers are not all created equal.
We should be clear if we're talking benchmarks which browser, which engine... in the Svelt context perhaps we only care about Node? Just a consideration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I tested on Node. This is for server-side code, so no need to test in browsers.
Thanks for sharing your findings! It may well have been different in the past like you said and regardless it was valuable to test various ideas and ensure we're doing as well as possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh for sure... we can certainly chase the peak performance (and probably should)... but we just may have to do so again in another 2 years - and make sure we're always comparing apples to apples. :). If we only need to test on Node that certainly helps a lot. Though it might not surprise me to learn if there were say differences between Node on ARM vs Node on x86_64...
Co-authored-by: Conduitry <git@chor.date>
According to an untrustworthy benchmark I just cobbled together... iterations = 1e7;
function number() {
console.time('number');
let total = 0;
let i = iterations;
while (i--) {
const n = i % 2 ? 0 : 1;
if (n) {
total += 1;
}
}
console.timeEnd('number');
return total;
}
function boolean() {
console.time('boolean');
let total = 0;
let i = iterations;
while (i--) {
const b = i % 2 ? false : true;
if (b) {
total += 1;
}
}
console.timeEnd('boolean');
return total;
} ...the boolean is indeed reliably faster. TIL! My assumption had always been that we'd still be better off, since minifiers typically rewrite function coerced_number() {
console.time('coerced_number');
let total = 0;
let i = iterations;
while (i--) {
const n = i % 2 ? !1 : !0;
if (n) {
total += 1;
}
}
console.timeEnd('coerced_number');
return total;
} For whatever reason, |
Few minor things:
|
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've updated this PR and addressed all the comments. LGTM now
It seems Svelte doesn't escape invalid surrogate pairs — I wonder if this should go in this PR? To be clear, it seems niche enough that it was never an issue, so the alternative could be to just not deal with it. |
I'm not entirely sure I understand the issue, but it sounds outside the scope of this PR |
Interesting, is it possible to use indexOf instead RegExp here? Let's see... |
This is a very late comment but wouldn't this be even faster if regular expressions are skipped entirely? https://jsbench.me/g0l5yr4wge/2 EDIT: |
@intrnl Looks like |
Maybe the best is to choose a method based on string length? Though I imagine that could vary based on hardware |
I'm not sure about the indexOf approach, as that would mean reiterating through the entire string twice ( I brought this up again mainly because I'm rather interested on seeing what people commonly interpolate, is it short? is it long? does it have a ton of instances of |
Before submitting the PR, please make sure you do the following
It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcsIdeally, include a test that fails without this PR but passes with it.Inspired by ryansolid/dom-expressions#27, this replaces the current
escape
function, used in SSR to escape<
,"
and&
characters, with a much faster version. It's not the same function in that PR, it's a smaller and (in my measurements) faster one that skips ahead to the next escapable character rather than incrementing one character at a time.It has quite a remarkable impact on performance. Using the marko-js/isomorphic-ui-benchmarks repo, we see the following results:
It no longer escapes
>
and'
characters because as far as I can tell that's not necessary. No tests needed to change.Tests
npm test
and lint the project withnpm run lint