Jonkman Microblog
  • Login
Show Navigation
  • Public

    • Public
    • Network
    • Groups
    • Popular
    • People

Notices by Sorin Davidoi (sorin@toot.cafe)

  1. Nolan (nolan@toot.cafe)'s status on Saturday, 31-Aug-2019 13:09:18 EDT Nolan Nolan

    I'd like to write a thread about some of the performance tricks in #Pinafore, just for those who might be interested.

    In conversation Saturday, 31-Aug-2019 13:09:18 EDT from toot.cafe permalink Repeated by sorin
  2. Nolan (nolan@toot.cafe)'s status on Friday, 30-Aug-2019 10:29:47 EDT Nolan Nolan

    "It takes a Village: Lessons on Collaborative Web Development" by Harald Kirschner https://www.youtube.com/watch?v=THTONE33mD8

    There is some really neat stuff coming up in the Firefox Dev Tools. Skip to 20:50 and see a really amazing demo of time-travel debugging. The moment where he adds logpoints and it prints a console.log *for previous points in time* blew my mind.

    In conversation Friday, 30-Aug-2019 10:29:47 EDT from toot.cafe permalink Repeated by sorin

    Attachments

    1. It takes a Village: Lessons on Collaborative Web Development
      By SFHTML5 from YouTube
  3. Sorin Davidoi (sorin@toot.cafe)'s status on Wednesday, 14-Aug-2019 14:27:44 EDT Sorin Davidoi Sorin Davidoi
    • Eugen

    @Gargron Hey, you probably need to copy the worker script in /static (either manually or via something like copy-webpack-plugin) and then pass that path to the class.

    In conversation Wednesday, 14-Aug-2019 14:27:44 EDT from toot.cafe permalink
  4. Sorin Davidoi (sorin@toot.cafe)'s status on Wednesday, 14-Aug-2019 11:41:08 EDT Sorin Davidoi Sorin Davidoi
    • Eugen

    @Gargron It can probably be lazy-loaded.

    In conversation Wednesday, 14-Aug-2019 11:41:08 EDT from toot.cafe permalink
  5. Sorin Davidoi (sorin@toot.cafe)'s status on Sunday, 11-Aug-2019 16:43:43 EDT Sorin Davidoi Sorin Davidoi
    • Andy Broomfield

    @andybroomfield Don't think it does.

    In conversation Sunday, 11-Aug-2019 16:43:43 EDT from toot.cafe permalink
  6. Nolan (nolan@toot.cafe)'s status on Sunday, 11-Aug-2019 13:42:48 EDT Nolan Nolan

    New blog post: "High-performance input handling on the web" https://nolanlawson.com/2019/08/11/high-performance-input-handling-on-the-web/

    In conversation Sunday, 11-Aug-2019 13:42:48 EDT from toot.cafe permalink Repeated by sorin

    Attachments

    1. High-performance input handling on the web
      By Nolan Lawson from Read the Tea Leaves

      Update: In a follow-up post, I explore some of the subtleties across browsers in how they fire input events.

      There is a class of UI performance problems that arise from the following situation: An input event is firing faster than the browser can paint frames.

      Several events can fit this description:

      • scroll
      • wheel
      • mousemove
      • touchmove
      • pointermove
      • etc.

      Intuitively, it makes sense why this would happen. A user can jiggle their mouse and deliver precise x/y updates faster than the browser can paint frames, especially if the UI thread is busy and thus the framerate is being throttled (also known as “jank”).

      In the above screenshot, pointermove events are firing faster than the framerate can keep up.[1] This can also happen for scroll events, touch events, etc.

      Update: In Chrome, pointermove is actually supposed to align/throttle to requestAnimationFrame automatically, but there is a bug where it behaves differently with Dev Tools open.

      The performance problem occurs when the developer naïvely chooses to handle the input directly:

      element.addEventListener('pointermove', () => {
        doExpensiveOperation()
      })
      

      In a previous post, I discussed Lodash’s debounce and throttle functions, which I find very useful for these kinds of situations. Recently however, I found a pattern I like even better, so I want to discuss that here.

      Understanding the event loop

      Let’s take a step back. What exactly are we trying to achieve here? Well, we want the browser to do only the work necessary to paint the frames that it’s able to paint. For instance, in the case of a pointermove event, we may want to update the x/y coordinates of an element rendered to the DOM.

      The problem with Lodash’s throttle()/debounce() is that we would have to choose an arbitrary delay (e.g. 20 milliseconds or 50 milliseconds), which may end up being faster or slower than the browser is actually able to paint, depending on the device and browser. So really, we want to throttle to requestAnimationFrame():

      element.addEventListener('pointermove', () => {
        requestAnimationFrame(doExpensiveOperation)
      })
      

      With the above code, we are at least aligning our work with the browser’s event loop, i.e. firing right before style and layout are calculated.

      However, even this is not really ideal. Imagine that a pointermove event fires three times for every frame. In that case, we will essentially do three times the necessary work on every frame:

      This may be harmless if the code is fast enough, or if it’s only writing to the DOM. However, if it’s both writing to and reading from the DOM, then we will end up with the classic layout thrashing scenario,[2] and our rAF-based solution is actually no better than handling the input directly, because we recalculate the style and layout for every pointermove event.

      Note the style and layout recalculations in the purple blocks, which Chrome marks with a red triangle and a warning about “forced reflow.”

      Throttling based on framerate

      Again, let’s take a step back and figure out what we’re trying to do. If the user is dragging their finger across the screen, and pointermove fires 3 times for every frame, then we actually don’t care about the first and second events. We only care about the third one, because that’s the one we need to paint.

      So let’s only run the final callback before each requestAnimationFrame. This pattern will work nicely:

      function throttleRAF () {
        let queuedCallback
        return callback => {
          if (!queuedCallback) {
            requestAnimationFrame(() => {
              const cb = queuedCallback
              queuedCallback = null
              cb()
            })
          }
          queuedCallback = callback
        }
      }
      

      We could also use cancelAnimationFrame for this, but I prefer the above solution because it’s calling fewer DOM APIs. (It only calls requestAnimationFrame() once per frame.)

      This is nice, but at this point we can still optimize it further. Recall that we want to avoid layout thrashing, which means we want to batch all of our reads and writes to avoid unnecessary recalculations.

      In “Accurately measuring layout on the web”, I explore some patterns for queuing a timer to fire after style and layout are calculated. Since writing that post, a new web standard called requestPostAnimationFrame has been proposed, and it fits the bill nicely. There is also a good polyfill called afterframe.

      To best align our DOM updates with the browser’s event loop, we want to follow these simple rules:

      1. DOM writes go in requestAnimationFrame().
      2. DOM reads go in requestPostAnimationFrame().

      The reason this works is because we write to the DOM right before the browser will need to calculate style and layout (in rAF), and then we read from the DOM once the calculations have been made and the DOM is “clean” (in rPAF).

      If we do this correctly, then we shouldn’t see any warnings in the Chrome Dev Tools about “forced reflow” (i.e. a forced style/layout outside of the browser’s normal event loop). Instead, all layout calculations should happen during the regular event loop cycle.

      In the Chrome Dev Tools, you can tell the difference between a forced layout (or “reflow”) and a normal one because of the red triangle (and warning) on the purple style/layout blocks. Note that above, there are no warnings.

      To accomplish this, let’s make our throttler more generic, and create one that can handle requestPostAnimationFrame as well:

      function throttle (timer) {
        let queuedCallback
        return callback => {
          if (!queuedCallback) {
            timer(() => {
              const cb = queuedCallback
              queuedCallback = null
              cb()
            })
          }
          queuedCallback = callback
        }
      }
      

      Then we can create multiple throttlers based on whether we’re doing DOM reads or writes:[3]

      const throttledWrite = throttle(requestAnimationFrame)
      const throttledRead = throttle(requestPostAnimationFrame)
      
      element.addEventListener('pointermove', e => {
        throttledWrite(() => {
          doWrite(e)
        })
        throttledRead(() => {
          doRead(e)
        })
      })
      

      Effectively, we have implemented something like fastdom, but using only requestAnimationFrame and requestPostAnimationFrame!

      Pointer event pitfalls

      The last piece of the puzzle (at least for me, while implementing a UI like this), was to avoid the pointer events polyfill. I found that, even after implementing all the above performance improvements, my UI was still janky in Firefox for Android.

      After some digging with WebIDE, I found that Firefox for Android currently does not support Pointer Events, and instead only supports Touch Events. (This is similar to the current version of iOS Safari.) After profiling, I found that the polyfill itself was taking up a lot of my frame budget.

      So instead, I switched to handling pointer/mouse/touch events myself. Hopefully in the near future this won’t be necessary, and all browsers will support Pointer Events! We’re already close.

      Here is the before-and-after of my UI, using Firefox on a Nexus 5:

       

      When handling very performance-sensitive scenarios, like a UI that should respond to every pointermove event, it’s important to reduce the amount of work done on each frame. I’m sure that this polyfill is useful in other situations, but in my case, it was just adding too much overhead.

      One other optimization I made was to delay updates to the store (which trigger some extra JavaScript computations) until the user’s drag had completed, instead of on every drag event. The end result is that, even on a resource-constrained device like the Nexus 5, the UI can actually keep up with the user’s finger!

      Conclusion

      I hope this blog post was helpful for anyone handling scroll, touchmove, pointermove, or similar input events. Thinking in terms of how I’d like to align my work with the browser’s event loop (using requestAnimationFrame and requestPostAnimationFrame) was useful for me.

      Note that I’m not saying to never use Lodash’s throttle or debounce. I use them all the time! Sometimes it makes sense to just let a timer fire every n milliseconds – e.g. when debouncing window resize events. In other cases, I like using requestIdleCallback – for instance, when updating a non-critical part of the UI based on user input, like a “number of characters remaining” counter when typing into a text box.

      In general, though, I hope that once requestPostAnimationFrame makes its way into browsers, web developers will start to think more purposefully about how they do UI updates, leading to fewer instances of layout thrashing. fastdom was written in 2013, and yet its lessons still apply today. Hopefully when rPAF lands, it will be much easier to use this pattern and reduce the impact of layout thrashing on web performance.

      Footnotes

      1. In the Pointer Events Level 2 spec, it says that pointermove events “may be coalesced or aligned to animation frame callbacks based on UA decision.” So hypothetically, a browser could throttle pointermove to fire only once per rAF (and if you need precise x/y events, e.g. for a drawing app, you can use getCoalescedEvents()). It’s not clear to me, though, that any browser actually does this. Update: see comments below, some browsers do! In any case, throttling the events to rAF in JavaScript accomplishes the same thing, regardless of UA behavior.

      2. Technically, the only DOM reads that matter in the case of layout thrashing are DOM APIs that force style/layout, e.g. getBoundingClientRect() and offsetLeft. If you’re just calling getAttribute() or classList.contains(), then you’re not going to trigger style/layout recalculations.

      3. Note that if you have different parts of the code that are doing separate reads/writes, then each one will need its own throttler function. Otherwise one throttler could cancel the other one out. This can be a bit tricky to get right, although to be fair the same footgun exists with Lodash’s debounce/throttle.

  7. Sorin Davidoi (sorin@toot.cafe)'s status on Sunday, 11-Aug-2019 11:49:06 EDT Sorin Davidoi Sorin Davidoi

    Love the new #Firefox nightly logo.

    In conversation Sunday, 11-Aug-2019 11:49:06 EDT from toot.cafe permalink
  8. Pinafore (pinafore@mastodon.technology)'s status on Saturday, 10-Aug-2019 13:54:15 EDT Pinafore Pinafore

    #Pinafore users: do you use the "show large inline images and videos" setting?

    [ ] Yes [ ] No
    In conversation Saturday, 10-Aug-2019 13:54:15 EDT from mastodon.technology permalink Repeated by sorin
  9. Sorin Davidoi (sorin@toot.cafe)'s status on Monday, 05-Aug-2019 07:12:04 EDT Sorin Davidoi Sorin Davidoi
    • Nolan
    • Thomas Steiner

    @nolan @tomayac https://blog.jse.li/posts/chrome-76-incognito-filesystem-timing/

    In conversation Monday, 05-Aug-2019 07:12:04 EDT from toot.cafe permalink
  10. Sorin Davidoi (sorin@toot.cafe)'s status on Thursday, 01-Aug-2019 10:28:53 EDT Sorin Davidoi Sorin Davidoi
    • Eugen
    • 🎓 Dr. Freemo :jpf: 🇳🇱

    @Gargron @freemo https://github.com/rust-lang-nursery/mdBook is quite similar and open source.

    In conversation Thursday, 01-Aug-2019 10:28:53 EDT from toot.cafe permalink

    Attachments

    1. rust-lang-nursery/mdBook
      from GitHub
      Create book from markdown files. Like Gitbook but implemented in Rust - rust-lang-nursery/mdBook
  11. Sorin Davidoi (sorin@toot.cafe)'s status on Wednesday, 26-Jun-2019 12:21:48 EDT Sorin Davidoi Sorin Davidoi

    Installing BIOS updates from the Gnome Software Center still feels surreal.

    In conversation Wednesday, 26-Jun-2019 12:21:48 EDT from toot.cafe permalink
  12. Dee 🧡 (dee@fedi.underscore.world)'s status on Sunday, 16-Jun-2019 14:06:13 EDT Dee 🧡 Dee 🧡
    The year is 5019. Humans, as we know them, are long gone. The Earth is inhabited chiefly by advanced, sapient machines.

    For legacy reasons, everyone's name starts with "Mozilla/5.0 (compatible;".
    In conversation Sunday, 16-Jun-2019 14:06:13 EDT from fedi.underscore.world permalink Repeated by sorin
  13. Nolan (nolan@toot.cafe)'s status on Friday, 14-Jun-2019 23:27:21 EDT Nolan Nolan

    "The New Wilderness" by Maciej Ceglowski https://idlewords.com/2019/06/the_new_wilderness.htm

    "[T]he giant tech companies can make a credible claim to be the defenders of privacy, just like a dragon can truthfully boast that it is good at protecting its hoard of gold."

    In conversation Friday, 14-Jun-2019 23:27:21 EDT from toot.cafe permalink Repeated by sorin
  14. Sorin Davidoi (sorin@toot.cafe)'s status on Friday, 07-Jun-2019 15:49:33 EDT Sorin Davidoi Sorin Davidoi

    You know it's an application for DACH when you need to fill in your title.

    In conversation Friday, 07-Jun-2019 15:49:33 EDT from toot.cafe permalink
  15. Sorin Davidoi (sorin@toot.cafe)'s status on Thursday, 06-Jun-2019 13:26:57 EDT Sorin Davidoi Sorin Davidoi

    No to marketing.

    In conversation Thursday, 06-Jun-2019 13:26:57 EDT from toot.cafe permalink
  16. Nolan (nolan@toot.cafe)'s status on Sunday, 02-Jun-2019 13:34:05 EDT Nolan Nolan

    New blog post: "One year of Pinafore" https://nolanlawson.com/2019/06/02/one-year-of-pinafore/

    My reflections on the last year (ish) of working on #Pinafore.

    In conversation Sunday, 02-Jun-2019 13:34:05 EDT from toot.cafe permalink Repeated by sorin
  17. Sorin Davidoi (sorin@toot.cafe)'s status on Tuesday, 28-May-2019 05:47:21 EDT Sorin Davidoi Sorin Davidoi
    • Nolan
    • SoapDog ✓✓✓ 3 times verified!

    @soapdog @nolan Wasn't it intentionally build like that? Otherwise we would have ended up with AppCache all over again.

    In conversation Tuesday, 28-May-2019 05:47:21 EDT from toot.cafe permalink
  18. Sorin Davidoi (sorin@toot.cafe)'s status on Monday, 27-May-2019 08:52:57 EDT Sorin Davidoi Sorin Davidoi
    • rabimba

    @rabimba Great I'll try to hunt them down later today and give it a go.

    In conversation Monday, 27-May-2019 08:52:57 EDT from toot.cafe permalink
  19. Sorin Davidoi (sorin@toot.cafe)'s status on Monday, 27-May-2019 02:55:00 EDT Sorin Davidoi Sorin Davidoi

    Lack of field validation for the Twitter handler field.

    In conversation Monday, 27-May-2019 02:55:00 EDT from toot.cafe permalink
  20. Sorin Davidoi (sorin@toot.cafe)'s status on Friday, 24-May-2019 11:15:31 EDT Sorin Davidoi Sorin Davidoi

    Let's see how this goes.

    In conversation Friday, 24-May-2019 11:15:31 EDT from toot.cafe permalink
  • Before
  • Help
  • About
  • FAQ
  • TOS
  • Privacy
  • Source
  • Version
  • Contact

Jonkman Microblog is a social network, courtesy of SOBAC Microcomputer Services. It runs on GNU social, version 1.2.0-beta5, available under the GNU Affero General Public License.

Creative Commons Attribution 3.0 All Jonkman Microblog content and data are available under the Creative Commons Attribution 3.0 license.

Switch to desktop site layout.