fine-tune infinite scrolling list

This commit is contained in:
Nolan Lawson 2018-01-17 00:06:24 -08:00
parent eacf28317e
commit 9e111bfc5a
9 changed files with 62 additions and 15 deletions

5
package-lock.json generated
View file

@ -5848,6 +5848,11 @@
} }
} }
}, },
"requestidlecallback": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/requestidlecallback/-/requestidlecallback-0.3.0.tgz",
"integrity": "sha1-b7dOBzP5DfP6pIOPn2oqX5t0KsU="
},
"require-directory": { "require-directory": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",

View file

@ -32,6 +32,7 @@
"npm-run-all": "^4.1.2", "npm-run-all": "^4.1.2",
"performance-now": "^2.1.0", "performance-now": "^2.1.0",
"pify": "^3.0.0", "pify": "^3.0.0",
"requestidlecallback": "^0.3.0",
"sapper": "^0.3.2", "sapper": "^0.3.2",
"serve-static": "^1.13.1", "serve-static": "^1.13.1",
"style-loader": "^0.19.1", "style-loader": "^0.19.1",

View file

@ -10,17 +10,20 @@
import Nav from './Nav.html'; import Nav from './Nav.html';
import { virtualListStore } from '../_utils/virtualListStore' import { virtualListStore } from '../_utils/virtualListStore'
import debounce from 'lodash/debounce'
import throttle from 'lodash/throttle' import throttle from 'lodash/throttle'
const THROTTLE_DELAY = 500
const SCROLL_EVENT_DELAY = 300
const RESIZE_EVENT_DELAY = 700
export default { export default {
oncreate() { oncreate() {
this.observe('innerHeight', throttle(() => { this.observe('innerHeight', debounce(() => {
// respond to window resize events // respond to window resize events
this.store.set({ this.store.set({
offsetHeight: this.refs.node.offsetHeight offsetHeight: this.refs.node.offsetHeight
}) })
}, THROTTLE_DELAY)) }, RESIZE_EVENT_DELAY))
this.store.set({ this.store.set({
scrollTop: this.refs.node.scrollTop, scrollTop: this.refs.node.scrollTop,
scrollHeight: this.refs.node.scrollHeight, scrollHeight: this.refs.node.scrollHeight,
@ -33,7 +36,10 @@
store: () => virtualListStore, store: () => virtualListStore,
events: { events: {
scroll(node, callback) { scroll(node, callback) {
const onScroll = throttle(callback, THROTTLE_DELAY) const onScroll = throttle(callback, SCROLL_EVENT_DELAY, {
leading: true,
trailing: true
})
node.addEventListener('scroll', onScroll); node.addEventListener('scroll', onScroll);
return { return {

View file

@ -14,7 +14,7 @@
import fixture from '../_utils/fixture.json' import fixture from '../_utils/fixture.json'
import StatusListItem from './StatusListItem.html' import StatusListItem from './StatusListItem.html'
import VirtualList from './VirtualList.html' import VirtualList from './VirtualList.html'
import { splice } from 'svelte-extras' import { splice, push } from 'svelte-extras'
let i = -1 let i = -1
@ -35,12 +35,33 @@
}, },
methods: { methods: {
splice: splice, splice: splice,
push: push,
addMoreItems() { addMoreItems() {
console.log('addMoreItems') console.log('addMoreItems')
let statuses = this.get('statuses') let statuses = this.get('statuses')
if (statuses) { if (statuses) {
this.splice('statuses', statuses.length, 0, ...createData()) let itemsToAdd = createData()
if (itemsToAdd.length) {
}
let importantFirstItem = itemsToAdd
this.splice('statuses', statuses.length, 0, ...itemsToAdd)
} }
},
addTheseItems(items) {
if (!items.length) {
return
}
this.push(items.pop())
while (items.length) {
this.addItemLazily(items.pop())
}
},
addItemLazily(item) {
requestIdleCallback(() => {
this.push(item)
})
} }
} }
} }

View file

@ -1,5 +1,4 @@
<div class="virtual-list"> <div class="virtual-list" style="height: {{$height}}px;">
<!-- <div class="virtual-list-viewport" ref:viewport></div> -->
{{#each $visibleItems as item @key}} {{#each $visibleItems as item @key}}
<VirtualListItem :component <VirtualListItem :component
offset="{{item.offset}}" offset="{{item.offset}}"
@ -27,9 +26,14 @@
}) })
}) })
let observedOnce = false
this.observe('distanceFromBottom', (distanceFromBottom) => { this.observe('distanceFromBottom', (distanceFromBottom) => {
//console.log('distanceFromBottom', distanceFromBottom) if (!observedOnce) {
if (distanceFromBottom > 0 && // hack: the first it's reported, it's always 0 observedOnce = true // TODO: the first time is always 0... need better way to handle this
return
}
if (distanceFromBottom >= 0 &&
distanceFromBottom <= DISTANCE_FROM_BOTTOM_TO_FIRE) { distanceFromBottom <= DISTANCE_FROM_BOTTOM_TO_FIRE) {
this.fire('scrollToBottom') this.fire('scrollToBottom')
} }

View file

@ -11,6 +11,7 @@
top: 0; top: 0;
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
/* will-change: transform; */ /* causes jank in mobile Firefox */
} }
.shown { .shown {
opacity: 1; opacity: 1;

View file

@ -17,8 +17,13 @@ const importIntersectionObserver = () => import(
/* webpackChunkname: 'intersection-observer' */ 'intersection-observer' /* webpackChunkname: 'intersection-observer' */ 'intersection-observer'
) )
const importRequestIdleCallback = () => import(
/* webpackChunkName: 'requestidlecallback' */ 'requestidlecallback'
)
export { export {
importURLSearchParams, importURLSearchParams,
importTimeline, importTimeline,
importIntersectionObserver importIntersectionObserver,
importRequestIdleCallback
} }

View file

@ -11,7 +11,7 @@ const virtualListStore = new VirtualListStore({
virtualListStore.compute('visibleItems', virtualListStore.compute('visibleItems',
['items', 'scrollTop', 'itemHeights', 'offsetHeight'], ['items', 'scrollTop', 'itemHeights', 'offsetHeight'],
(items, scrollTop, itemHeights, offsetHeight) => { (items, scrollTop, itemHeights, offsetHeight) => {
let renderBuffer = 1.5 * offsetHeight let renderBuffer = 3 * offsetHeight
let visibleItems = [] let visibleItems = []
let totalOffset = 0 let totalOffset = 0
let len = items.length let len = items.length

View file

@ -1,11 +1,15 @@
import { init } from 'sapper/runtime.js' import { init } from 'sapper/runtime.js'
import { importURLSearchParams } from '../routes/_utils/asyncModules' import {
import { importIntersectionObserver } from '../routes/_utils/asyncModules' importURLSearchParams,
importIntersectionObserver,
importRequestIdleCallback
} from '../routes/_utils/asyncModules'
// polyfills // polyfills
Promise.all([ Promise.all([
typeof URLSearchParams === 'undefined' && importURLSearchParams(), typeof URLSearchParams === 'undefined' && importURLSearchParams(),
typeof IntersectionObserver === 'undefined' && importIntersectionObserver() typeof IntersectionObserver === 'undefined' && importIntersectionObserver(),
typeof requestIdleCallback === 'undefined' && importRequestIdleCallback()
]).then(() => { ]).then(() => {
// `routes` is an array of route objects injected by Sapper // `routes` is an array of route objects injected by Sapper
init(document.querySelector('#sapper'), __routes__) init(document.querySelector('#sapper'), __routes__)