{"action":"created","issue":{"url":"https://api.github.com/repos/google/WebFundamentals/issues/2227","labels_url":"https://api.github.com/repos/google/WebFundamentals/issues/2227/labels{/name}","comments_url":"https://api.github.com/repos/google/WebFundamentals/issues/2227/comments","events_url":"https://api.github.com/repos/google/WebFundamentals/issues/2227/events","html_url":"https://github.com/google/WebFundamentals/issues/2227","id":113348006,"number":2227,"title":"Improve \"Debounce your scroll handlers\" and establish better best practice for \"Avoid layout thrashing\"","user":{"login":"aFarkas","id":188254,"avatar_url":"https://avatars.githubusercontent.com/u/188254?v=3","gravatar_id":"","url":"https://api.github.com/users/aFarkas","html_url":"https://github.com/aFarkas","followers_url":"https://api.github.com/users/aFarkas/followers","following_url":"https://api.github.com/users/aFarkas/following{/other_user}","gists_url":"https://api.github.com/users/aFarkas/gists{/gist_id}","starred_url":"https://api.github.com/users/aFarkas/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/aFarkas/subscriptions","organizations_url":"https://api.github.com/users/aFarkas/orgs","repos_url":"https://api.github.com/users/aFarkas/repos","events_url":"https://api.github.com/users/aFarkas/events{/privacy}","received_events_url":"https://api.github.com/users/aFarkas/received_events","type":"User","site_admin":false},"labels":[{"url":"https://api.github.com/repos/google/WebFundamentals/labels/section-fundamentals","name":"section-fundamentals","color":"d4c5f9"},{"url":"https://api.github.com/repos/google/WebFundamentals/labels/type-Content","name":"type-Content","color":"3f51b5"}],"state":"open","locked":false,"assignee":{"login":"paullewis","id":617438,"avatar_url":"https://avatars.githubusercontent.com/u/617438?v=3","gravatar_id":"","url":"https://api.github.com/users/paullewis","html_url":"https://github.com/paullewis","followers_url":"https://api.github.com/users/paullewis/followers","following_url":"https://api.github.com/users/paullewis/following{/other_user}","gists_url":"https://api.github.com/users/paullewis/gists{/gist_id}","starred_url":"https://api.github.com/users/paullewis/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/paullewis/subscriptions","organizations_url":"https://api.github.com/users/paullewis/orgs","repos_url":"https://api.github.com/users/paullewis/repos","events_url":"https://api.github.com/users/paullewis/events{/privacy}","received_events_url":"https://api.github.com/users/paullewis/received_events","type":"User","site_admin":false},"milestone":null,"comments":1,"created_at":"2015-10-26T12:49:21Z","updated_at":"2015-10-26T15:10:11Z","closed_at":null,"body":"The article and especially the code in \"[Debounce your scroll handlers](https://developers.google.com/web/fundamentals/performance/rendering/debounce-your-input-handlers?hl=en)\" has some issues and can be improved a lot. It has to be said, that there are many different use cases to listen for scroll/resize events and not all of them can be handled with the same pattern.\r\n\r\nAlso the article [Avoid large, complex layouts and layout thrashing\r\n](https://developers.google.com/web/fundamentals/performance/rendering/avoid-large-complex-layouts-and-layout-thrashing#avoid-layout-thrashing) can be improved by establishing a new best practice, that can be easily fulfilled in practice.\r\n\r\nSo the pattern used in this article looks like this:\r\n\r\n```js\r\nfunction onScroll (evt) {\r\n\r\n  // Store the scroll value for laterz.\r\n  lastScrollY = window.scrollY;\r\n\r\n  // Prevent multiple rAF callbacks.\r\n  if (scheduledAnimationFrame)\r\n    return;\r\n\r\n  scheduledAnimationFrame = true;\r\n  requestAnimationFrame(readAndUpdatePage);\r\n}\r\n```\r\n\r\nLet's go through it line by line.\r\n\r\n1. sync read in handler\r\n```js\r\n// Store the scroll value for laterz.\r\nlastScrollY = window.scrollY;\r\n```\r\n\r\nAccess to `scrollY` is a read operation in terms of style/layout and can be done at any time. While the article says you shouldn't read layout in input handler, the code just does this. This makes absolutely no sense. (I come back later to this point).\r\n\r\n2. Throttle something, that is already throttled the same way.\r\n\r\n```js\r\n// Prevent multiple rAF callbacks.\r\nif (scheduledAnimationFrame)\r\n  return;\r\n\r\nscheduledAnimationFrame = true;\r\n```\r\n\r\nWhile there are some old browsers out there, where the resize/scroll events go wild. All modern browsers  already throttle these events and this makes totally sense. A resize/scroll that is not painted, doesn't need to be dispatched.\r\n\r\nBecause a `requestAnimationFrame` callback is always scheduled **before** calculating the layout/style not only the handler but also everything inside of the rAf callback is blocking the browser from rendering the next frame.\r\n\r\nThis means the pattern doesn't really include any efficient throttling or debouncing.\r\n\r\nThis leaves us with the following pattern, which is async but not really debounced as described above:\r\n\r\n```js\r\nfunction onScroll (evt) {\r\n  requestAnimationFrame(readAndUpdatePage);\r\n}\r\n```\r\n\r\nA simple throttle pattern could look something like this:\r\n\r\n```JS\r\nfunction onScroll (evt) {\r\n\r\n  // Store the scroll value for laterz.\r\n  lastScrollY = window.scrollY;\r\n\r\n  // Prevent multiple rAF callbacks.\r\n  if (scheduledAsync)\r\n    return;\r\n\r\n  scheduledAsync = true;\r\n  setTimeout(readAndUpdatePage, 66);\r\n}\r\n```\r\n\r\n------\r\n\r\nBetter best practice to avoid layout thrashing\r\n\r\nThe pattern also suggest to readAndWrite layout in a requestAnimationFrame. With this you don't win anything. As soon as multiple different scripts are following this advice, you end up with read -> write -> read -> write layout thrashing inside of a `requestAnimationFrame`:\r\n\r\n```js\r\nfunction onScrollComponent1 (evt) {\r\n  requestAnimationFrame(readAndUpdatePage);\r\n}\r\n\r\nfunction onScrollComponent2 (evt) {\r\n  requestAnimationFrame(readAndUpdatePage);\r\n}\r\n```\r\n\r\nOf course you can say, that this has to be managed by another third party script like fastdom, but it is escapist that all scripts (especially those you don't own) in a page are using fastdom.\r\n\r\nIn fact there is a much simpler pattern to fully avoid layout thrashing, that can be established without the need of a library, that everybody has to use.\r\n\r\nIt's simple: **Put all your *writes*  inside a `requestAnimationFrame` and never *read* inside a `requestAnimationFrame`.\r\n\r\nIf you think about it, this makes much more sense. `requestAnimationFrame` main purpose was always to do animation, hence layout writes. If the browser has calculated the layout and it is not invalidated, *layout reads* are extremely fast no matter at which point there are called. But the point where you can be sure of it is the point right *after* layout calculation. `requestAnimationFrame` gives you the time right *before* layout calculation, which means it is the latest point to do layout reads before the browser would do layout himself, hence it is the most dangerous point to do reads. I know my explanation isn't so good, but I hope this makes it clear. See also [this issue](https://github.com/wilsonpage/fastdom/issues/65). If you have any doubts in this, just explain those. I will try to clear them.\r\n\r\nThis basically means for our debounce input handler article the following patterns:\r\n\r\n**Use case: Instant visual reaction to the event**\r\n```js\r\nfunction onScroll (evt) {\r\n\t// do all your reads...\r\n\tlastScrollY = window.scrollY;\r\n\t\r\n\t// do all your writes inside rAF\r\n\trequestAnimationFrame(updatePage);\r\n}\r\n\r\nwindow.addEventListener('scroll', onScroll);\r\n```\r\n\r\n**Use case: lazy reaction to the event**\r\n\r\n```js\r\n// helper function (there are better ways to do it....)\r\nfunction throttle(fn, delay) {\r\n\tvar _this, args;\r\n\tvar scheduled = false;\r\n\tvar call = function () {\r\n\t\tscheduled = false;\r\n\t\tfn.apply(_this, args);\r\n\t};\r\n\treturn function() {\r\n\t\t_this = this;\r\n\t\targs = arguments;\r\n\t\tif (scheduled) {\r\n\t\t\treturn;\r\n\t\t}\r\n\t\tscheduled = true;\r\n\t\tsetTimeout(call, delay || 66);\r\n\t}\r\n}\r\n\r\n\r\nvar onScroll = throttle(function(evt) {\r\n\t// do all your reads...\r\n\tlastScrollY = window.scrollY;\r\n\r\n\t// do all your writes inside rAF\r\n\trequestAnimationFrame(updatePage);\r\n});\r\n\r\nwindow.addEventListener('scroll', onScroll);\r\n```\r\n\r\n**Use case: React after event is settled**\r\n\r\n```js\r\n// helper function\r\nfunction debounce(func, wait) {\r\n\t// we need to save these in the closure\r\n\tvar timeout, args, context, timestamp;\r\n\r\n\t// this is where the magic happens\r\n\tvar later = function() {\r\n\r\n\t\t// how long ago was the last call\r\n\t\tvar last = Date.now() - timestamp;\r\n\r\n\t\t// if the latest call was less that the wait period ago\r\n\t\t// then we reset the timeout to wait for the difference\r\n\t\tif (last < wait) {\r\n\t\t\ttimeout = setTimeout(later, wait - last);\r\n\r\n\t\t\t// or if not we can null out the timer and run the latest\r\n\t\t} else {\r\n\t\t\ttimeout = null;\r\n\t\t\tfunc.apply(context, args);\r\n\t\t}\r\n\t};\r\n\r\n\treturn function() {\r\n\r\n\t\t// save details of latest call\r\n\t\tcontext = this;\r\n\t\targs = arguments;\r\n\t\ttimestamp = Date.now();\r\n\r\n\t\t// we only need to set the timer now if one isn't already running\r\n\t\tif (!timeout) {\r\n\t\t\ttimeout = setTimeout(later, wait);\r\n\t\t}\r\n\t}\r\n}\r\n\r\n\r\nvar onScroll = debounce(function(evt) {\r\n\t// do all your reads...\r\n\tlastScrollY = window.scrollY;\r\n\r\n\t// do all your writes inside rAF\r\n\trequestAnimationFrame(updatePage);\r\n});\r\n\r\nwindow.addEventListener('scroll', onScroll);\r\n```\r\n\r\nOf course you can also add a forth pattern using a mixture of throttle and `requestIdleCallback`."},"comment":{"url":"https://api.github.com/repos/google/WebFundamentals/issues/comments/151168413","html_url":"https://github.com/google/WebFundamentals/issues/2227#issuecomment-151168413","issue_url":"https://api.github.com/repos/google/WebFundamentals/issues/2227","id":151168413,"user":{"login":"petele","id":718038,"avatar_url":"https://avatars.githubusercontent.com/u/718038?v=3","gravatar_id":"","url":"https://api.github.com/users/petele","html_url":"https://github.com/petele","followers_url":"https://api.github.com/users/petele/followers","following_url":"https://api.github.com/users/petele/following{/other_user}","gists_url":"https://api.github.com/users/petele/gists{/gist_id}","starred_url":"https://api.github.com/users/petele/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/petele/subscriptions","organizations_url":"https://api.github.com/users/petele/orgs","repos_url":"https://api.github.com/users/petele/repos","events_url":"https://api.github.com/users/petele/events{/privacy}","received_events_url":"https://api.github.com/users/petele/received_events","type":"User","site_admin":false},"created_at":"2015-10-26T15:10:11Z","updated_at":"2015-10-26T15:10:11Z","body":"@paullewis - can you please take a look and adjust as necessary.  thanks!"}}