Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

This is fun.

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 :)


I wonder how `<canvas>` would compare to `<img src="data:image/svg..."/>` instead


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!


Speaking performance/optimisations, interesting choice to destroy canvas when it leaves viewport and recreate it upon each re-entry.

Personally I'd leave it created and stop observing the element instead, à la

    /* … */ if (entry.isIntersecting) { observer.unobserve(entry.target); /* … */ }
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?)


That's a really good idea. Definitely something to measure, but I suspect you're right.


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.


This is great, thanks. A small fix I made to stop console warnings was to change:

  x.fillStyle = '#' + ((r >> 8) & 0xffffff).toString(16).padStart(0, 6);
to:

  x.fillStyle = '#' + ((r >> 8) & 0xffffff).toString(16).padStart(6, '0');
as the pad target length is the first parameter of padStart.


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.


Or just generate them on page load and call it a day. No bugs, no complexity, no edge cases.

Could it be faster? Sure, but should it be faster? Not really, IMO, the avatars load very quick, even on a big page.


Awesome! I did not know about IntersectionObserver. Will def add this to my toolbox! Thanks!




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: