tjstretchalot's recent activity

  1. Comment on Introducing Hurl, a terrible (but cute) idea for a language in ~comp

    tjstretchalot
    Link
    I'm actually quite excited to try this out if an interpreter ever comes out - I imagine toss/return will require some crazy stack manipulation, so whatever you end up with will lead to...

    I'm actually quite excited to try this out if an interpreter ever comes out - I imagine toss/return will require some crazy stack manipulation, so whatever you end up with will lead to implementations that would never exist otherwise. Kind of like sic1

  2. Comment on Beehaw.org: defederating effective immediately from lemmy.world and sh.itjust.works in ~tech

    tjstretchalot
    Link
    I think of federation as a slightly less extreme case of cryptocurrency: it's a very interesting technical challenge, and it does come with some theoretical improvements. However, it's plagued by...

    I think of federation as a slightly less extreme case of cryptocurrency: it's a very interesting technical challenge, and it does come with some theoretical improvements. However, it's plagued by practical issues (performance with mismatched server sizes), and generally fails to do anything an appropriately managed centralized solution couldn't do.

    Federation by default makes it impossible to have any checks on new accounts: if I want an account, I can just make a federated instance and an account there. Thus, any instance which wants anything more than that will necessarily remove federation by default, as done here.

    Opt-in federation implies coordination, i.e., a mutually respecting relationship between the communities. Typically, it would also involve agreeing to a similar set of rules on both sides, a way to resolve disagreements, etc. And when this type of relationship is in place, conventional tools offer as much as federation with a much simpler implementation. This simplicity means that more time can be spent on implementing how the communities wish to be merged, and less time handling sync issues

    14 votes
  3. Comment on Making infinite scrollable lists for web without a constantly expanding DOM in ~comp

    tjstretchalot
    Link Parent
    Wow these are super interesting projects - thank you for the terminology unlock. My initial reaction looking at these is that react-virtualized has been replaced with react-window, where...

    Wow these are super interesting projects - thank you for the terminology unlock. My initial reaction looking at these is that react-virtualized has been replaced with react-window, where react-window is more narrow in scope. react-virtuoso is definitely a more confusing name and a worse README, but looking at the output of two, a better approach.

    It's notable that react-window and react-virtuoso use different strategies. While I was working on this, I initially used the strategy by react-window: absolutely positioning elements. However, I struggled when elements get resized using this paradigm, so I'm interested in seeing how it handles that. react-virtuoso uses the solution I eventually settled on: padding the top and bottom of the element but allowing browser scrolling. This also dramatically reduces the amount of DOM manipulation required, which is always positive.

    One notable characteristic of react-virtuoso is that it disables overflow anchoring on all the child elements, over the more obvious behavior of just disabling it on the parent. This is a bit perplexing as there aren't that many browser versions that could have had a bug related to this.

    I'll definitely read over the source code of react-virtuoso and react-window to extract what tricks they are using. Seeing that there's a full axis-aligned tree implementation in react-virtuoso makes me quite curious.

    One thing I'll be looking at with a keen eye is some wordage seems to suggest that it cares about elements that are significantly off-screen: that would suggest the algorithm scales to some degree based on the index, which isn't necessary: I don't see why there should be any difference performance or memory wise based on if you've scrolled 1,000,000 elements or 100.

  4. Making infinite scrollable lists for web without a constantly expanding DOM

    A common theme in web development, and the crux of the so-called "Web 2.0" is scrolling through dynamic lists of content. Tildes is such an example: you can scroll through about 50 topics on the...

    A common theme in web development, and the crux of the so-called "Web 2.0" is scrolling through dynamic lists of content. Tildes is such an example: you can scroll through about 50 topics on the front page before you reach a "next" button if you want to keep looking.

    There's a certain beauty in the simplicity of the next/previous page. When done right it's fast, it's easy, and fits neatly into a server-side rendered model. However, it does cause that small bit of friction where you need to hit the next button to go forward -- taking you out of the "flow", so-to-speak. It's slick, but it could be slicker. Perhaps more importantly, it's an interesting problem to solve.

    A step up from the next/previous button is to load the next page of content when you reach the end of the list, inserting it below. If the load is pretty fast, this will hardly interrupt your flow at all! The ever-so-popular reddit enhancement suite does precisely that for reddit: instead of a next button, when you reach the bottom, the next page of items simply plops into place. If the loading isn't fast enough, perhaps instead of loading when they reach the last item, you might choose to load when they hit the fifth from last item, etc.

    To try to keep this post more concrete, and more helpful, here's how this type of pagination would work in practice, in typescript and using the Intersection Observer API but otherwise framework agnostic:

    /**
     * Allows the user to scroll forever through the given list by calling the given loadMore()
     * function whenever the bottom element (by default) becomes visible. This assumes that
     * loadMore is the only thing that modifies the list, and that the list is done being modified
     * once the promise returned from loadMore resolves
     *
     * @param list The element which contains the individual items
     * @param loadMore A function which can be called to insert more items into the list. Can return
     *   a rejected promise to indicate that there are no more items to load
     * @param triggerLoadAt The index of the child in the list which triggers the load. Negative numbers
     *   are interpreted as offsets from the end of the list. 
     */
    function handlePagination(list: Element, loadMore: () => Promise<void>, triggerLoadAt: number = -1) {
        manageIntersection();
        return;
    
        function handleIntersection(ele: Element, handler: () => void): () => void {
            let active = true;
            const observer = new IntersectionObserver((entries) => {
                if (active && entries[0].isIntersecting) {
                    handler()
                }
            }, { root: null, threshold: 0.5 });
            observer.observe(ele);
            return () => {
                if (active) {
                    active = false;
                    observer.disconnect();
                }
            }
        }
    
        function manageIntersection() {
            const index = triggerLoadAt < 0 ? list.children.length + triggerLoadAt : triggerLoadAt;
            if (index < 0 || index >= list.children.length) {
                throw new Error(`index=${index} is not valid for a list of ${list.children.length} items`);
            }
    
            const child = list.children[index];
            const removeIntersectionHandler = handleIntersection(child, () => {
                removeIntersectionHandler();
                loadMore().then(() => {
                    manageIntersection();
                }).catch((e) => {});
            });
        }
    }
    

    If you're sane, this probably suffices for you. However, there is still one problem: as you scroll,
    the number of elements on the DOM get longer and longer. This means they necessarily take up
    some amount of memory, and browsers probably have to do some amount of work to keep
    track of them. Thus, in theory, if you were to scroll long enough, the page would get slower and
    slower! How long "long enough" is would depend mostly on how complicated each item is: if each one
    is a unique 20k element svg, it'll get slow pretty quickly.

    The trick to avoid this, and to get a constant overhead, is that when adding new items below, remove the same number of items above! Of course, if the user scrolls back up they'll be expecting those items to be there, but no worries, the handlePagination from before works just as well for loading items before the first item.

    However, this simple change is where a key problem arises: inserting elements below doesn't cause any layout shift, but inserting an item above ought to--right?

    The answer is: it depends on the browser! Back in 2017 chrome realized that it's often convenient to be able to insert items into the dom above the viewport, and implemented scroll anchoring, which basically ensures that if you insert an item 50px tall above the viewport, then scroll 50px down so that there's no visual layout shift. Firefox followed suite in 2019, and edge got support in 2020. But alas, safari both on mac and ios does not support scroll anchoring (though they expressed interest in it since 2017)

    Now, there's two responses to this:

    • Surely Safari support is coming soon, they've posted on that bug as recently as April! Just use simpler pagination for now
    • Pshhhh, just implement scroll anchoring ourself!

    Of course, I've gone and done #2, and it almost perfectly works. Here's the idea:

    • Right before loadMore, find the first item in the list which is inside the viewport. This is the item whose position we don't want to move. Use getBoundingClientRect to find it's top position.
    • Perform the DOM manipulation as desired
    • Use getBoundingClientRect again to find the new top of that item.
    • Insert (or remove) the appropriate amount of blank space at the top of the list to offset the change in client rect (note that if there's scroll anchoring support in the browser this should always be zero, which means this effectively works as progressive enhancement)

    Now, the function to do this is a tad too long for this post. I implemented it in React, however, and combined it with some stronger preloading object (we don't need all the items we've fetched from the API on the DOM, so we can use before, onTheDom, after lists to avoid getting a bunch of api requests just from scrolling down and up within the same small number of items).

    What's interesting is that it still works perfectly on chrome even with scroll-anchoring disabled (via overflow-anchor: none), but on Safari there is still, sometimes, 1 frame where it renders the wrong scroll position before immediately adjusting. Because I implemented it in react, however, my current hypothesis is I have a mistake somewhere which causes the javascript to yield to the renderer before all the manipulations are done, and it only shows up on Safari because of the generally higher framerates there

    If it's interesting to people, I could extract the infinite list component outside of this project: I certainly like it, and in my case I do expect people to want to quickly scroll through hundreds to thousands of items, so the lighter DOM feels worth it (though perhaps it wouldn't if I had known, when starting, how painful getting it to work on Safari would be!).

    What do you think of this type of "true" infinite scrolling for web? Good thing, neutral thing, bad thing? Would you use it, if the component were available? Would you remove it, if you saw someone doing this? Are there other questions about how this was accomplished? Is this an appropriate post for Tildes?

    11 votes