One suggestion.. rather than creating a canvas for each user using a querySelectorAll and a loop, I'd use an IntersectionObserver and only create the canvases as they scroll into view. That way the user's device won't need to create hundreds of elements when the code runs.
let observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry, i) => {
if (entry.isIntersecting) {
const p = 2;
const c = document.createElement('canvas');
const x = c.getContext('2d');
c.width = 18;
c.height = 14;
const s = entry.target.innerText;
const r = 1;
if (s) {
for (
let s = entry.target.innerText, r = 1, i = 28 + s.length;
i--;
) {
// xorshift32
(r ^= r << 13), (r ^= r >>> 17), (r ^= r << 5);
const X = i & 3,
Y = i >> 2;
if (i >= 28) {
// seed state
r += s.charCodeAt(i - 28);
x.fillStyle =
'#' + ((r >> 8) & 0xffffff).toString(16).padStart(0, 6);
} else {
// draw pixel
if (r >>> 29 > (X * X) / 3 + Y / 2)
x.fillRect(p * 3 + p * X, p * Y, p, p),
x.fillRect(p * 3 - p * X, p * Y, p, p);
}
}
}
entry.target.prepend(c);
} else {
if (entry.target.firstChild.tagName === 'CANVAS')
entry.target.firstChild.remove();
}
});
},
{ rootMargin: '0px 0px 0px 0px' }
);
document.querySelectorAll('.hnuser').forEach((user) => {
observer.observe(user);
});
Thanks, Interesting idea. I'm a little sceptical of the performance improvement, but I suppose that depends what we mean by performance.
Your strategy essentially trades a one time computation and DOM mutation with a continuous but lighter one with some added overhead. In this case I suspect the total power performance is worse over time; _however_ latency and UX should be _better_, since it removes the relationship between total comments on the page and the time to render avatars, which is currently about 100ms for this page on my machine.
I'm definitely biased towards preferring a one time change and making it as efficient as possible, but when the input has a high enough ceiling I can see how your strategy would make more sense - I'm not quite sure where I stand on HN threads - but thankfully this is just a user script so we can make our own choices :)
Yes! it did cross my mind, I was just more familiar with canvas API so I cracked on with it... but I suppose encoding a simple format like bitmap would be trivial, almost like being able to access the canvas pixel buffer but with a lower overhead.
This thread is a good perf test, I might give it a quick go.
Also as others mentions the pixelation style thing is now finally fully cross browser supported, which should make it easier to achieve nearest neighbour upscaling for url encoding.
[edit]
I underestimated how involved image format headers are!
Do you think (or know) that swapping observed node for canvas (potentially producing many canvases) is more expensive than keeping all observers and having smallest possible amount of canvases? (Maybe I'm biased towards saving CPU but saving RAM is better after all?)
If it turns out there’s some upper bound where caching canvases in memory becomes a problem (eg large threads become not paginated), it’s pretty trivial to build a cache around [Weak]Map and get the ~best of both worlds.
A better optimization would be adding appropriately sized SVG <use> tags to comments before first paint (to prevent re-flowing the layout) and lazily generate avatars to a SVG sprite sheet. No canvas required.
A nice improvement! An unfortunate side-effect of it is that it introduces some jitter during macOS's smooth scrolling in Firefox at time of first render, though.
Does the canvas need to be created and removed as it comes in and out of view? Using requestAnimationFrame() might also further improve responsiveness.
Does the canvas need to be created and removed as it comes in and out of view?
Not really. You could just create them and not bother removing them. You'd need to check if the user already had an avatar if you did that though, or you'd end up with lots of repeated avatars when you scroll up and down the page.
One suggestion.. rather than creating a canvas for each user using a querySelectorAll and a loop, I'd use an IntersectionObserver and only create the canvases as they scroll into view. That way the user's device won't need to create hundreds of elements when the code runs.