There are two cases here. I'll start with the simple one, which is when the number of rows is sufficiently low (< ~30'000) that the browser's engine can handle a DIV with a height set to the amount of space required for the entire grid (say 30'000 * 30px). In this case, we just set the height of the DIV to that and then listen to its onscroll events and dynamically (either synchronously or asynchronously, depending on the timings and scroll distance) add and remove absolutely-positioned rows that happen to be in and around the viewport. At no point is the scrolling controlled directly, which is one of the reasons it's so fast - the browser still does the scrolling at native speed fully utilizing bitmap caching and whatever hardware acceleration it has.
In the second case, i.e. when the number of rows is more than what the browser can handle, things get tricky. The canvas is 'virtualized' into overlapping panes. While scrolling within a single pane, everything works just like described above, except there is an position offset we apply when placing rows. When scrolling by the scrollbar thumb, we determine which pane the user stopped at, apply that offset and re-render. When scrolling locally, we detect when the pane jumps occur and do the same.