@@ -247,13 +247,13 @@ export class DomRepeat extends domRepeatBase {
247247 /**
248248 * Defines an initial count of template instances to render after setting
249249 * the `items` array, before the next paint, and puts the `dom-repeat`
250- * into "chunking mode". The remaining items will be created and rendered
251- * incrementally at each animation frame therof until all instances have
250+ * into "chunking mode". The remaining items (and any future items as a
251+ * result of pushing onto the array) will be created and rendered
252+ * incrementally at each animation frame thereof until all instances have
252253 * been rendered.
253254 */
254255 initialCount : {
255- type : Number ,
256- observer : '__initializeChunking'
256+ type : Number
257257 } ,
258258
259259 /**
@@ -285,6 +285,25 @@ export class DomRepeat extends domRepeatBase {
285285 */
286286 notifyDomChange : {
287287 type : Boolean
288+ } ,
289+
290+ /**
291+ * When chunking is enabled via `initialCount` and the `items` array is
292+ * set to a new array, this flag controls whether the previously rendered
293+ * instances are reused or not.
294+ *
295+ * When `true`, any previously rendered template instances are updated in
296+ * place to their new item values synchronously in one shot, and then any
297+ * further items (if any) are chunked out. When `false`, the list is
298+ * returned back to its `initialCount` (any instances over the initial
299+ * count are discarded) and the remainder of the list is chunked back in.
300+ * Set this to `true` to avoid re-creating the list and losing scroll
301+ * position, although note that when changing the list to completely
302+ * different data the render thread will be blocked until all existing
303+ * instances are updated to their new data.
304+ */
305+ reuseChunkedInstances : {
306+ type : Boolean
288307 }
289308
290309 } ;
@@ -303,7 +322,10 @@ export class DomRepeat extends domRepeatBase {
303322 this . __renderDebouncer = null ;
304323 this . __itemsIdxToInstIdx = { } ;
305324 this . __chunkCount = null ;
306- this . __lastChunkTime = null ;
325+ this . __renderStartTime = null ;
326+ this . __restartChunking = true ;
327+ this . __shouldMeasureChunk = false ;
328+ this . __shouldContinueChunking = false ;
307329 this . __sortFn = null ;
308330 this . __filterFn = null ;
309331 this . __observePaths = null ;
@@ -445,36 +467,58 @@ export class DomRepeat extends domRepeatBase {
445467 return Math . ceil ( 1000 / rate ) ;
446468 }
447469
448- __initializeChunking ( ) {
449- if ( this . initialCount ) {
450- this . __limit = this . initialCount ;
451- this . __chunkCount = this . initialCount ;
452- this . __lastChunkTime = performance . now ( ) ;
470+ __updateLimitAndScheduleNextChunk ( filteredItemCount ) {
471+ let newCount ;
472+ if ( this . __restartChunking ) {
473+ this . __restartChunking = false ;
474+ // Limit next render to the initial count
475+ this . __limit = Math . min ( filteredItemCount , this . initialCount ) ;
476+ // Subtract off any existing instances to determine the number of
477+ // instances that will be created
478+ newCount = Math . max ( this . __limit - this . __instances . length , 0 ) ;
479+ // Initialize the chunk size with how many items we're creating
480+ this . __chunkCount = newCount || 1 ;
481+ } else {
482+ // The number of new instances that will be created is based on the
483+ // existing instances, the new list size, and the maximum chunk size
484+ newCount = Math . min (
485+ Math . max ( filteredItemCount - this . __instances . length , 0 ) ,
486+ this . __chunkCount ) ;
487+ // Increase the limit based on how many new items we're making
488+ this . __limit = Math . min ( this . __limit + newCount , filteredItemCount ) ;
453489 }
454- }
455-
456- __tryRenderChunk ( ) {
457- // Debounced so that multiple calls through `_render` between animation
458- // frames only queue one new rAF (e.g. array mutation & chunked render)
459- if ( this . items && this . __limit < this . items . length ) {
460- this . __debounceRender ( this . __requestRenderChunk ) ;
490+ // Schedule a rAF task to measure the total time to render the chunk
491+ // (including layout/paint plus any other thread contention), throttle the
492+ // chunk size accordingly, and schedule another render if there is more to
493+ // chunk
494+ this . __shouldMeasureChunk = newCount === this . __chunkCount ;
495+ this . __shouldContinueChunking = this . __limit < filteredItemCount ;
496+ this . __renderStartTime = performance . now ( ) ;
497+ if ( this . __shouldMeasureChunk || this . __shouldContinueChunking ) {
498+ this . __debounceRender ( this . __continueChunkingAfterRaf ) ;
461499 }
462500 }
463501
464- __requestRenderChunk ( ) {
465- requestAnimationFrame ( ( ) => this . __renderChunk ( ) ) ;
502+ __continueChunkingAfterRaf ( ) {
503+ requestAnimationFrame ( ( ) => this . __continueChunking ( ) ) ;
466504 }
467505
468- __renderChunk ( ) {
506+ __continueChunking ( ) {
469507 // Simple auto chunkSize throttling algorithm based on feedback loop:
470- // measure actual time between frames and scale chunk count by ratio
471- // of target/actual frame time
472- let currChunkTime = performance . now ( ) ;
473- let ratio = this . _targetFrameTime / ( currChunkTime - this . __lastChunkTime ) ;
474- this . __chunkCount = Math . round ( this . __chunkCount * ratio ) || 1 ;
475- this . __limit += this . __chunkCount ;
476- this . __lastChunkTime = currChunkTime ;
477- this . __debounceRender ( this . __render ) ;
508+ // measure actual time between frames and scale chunk count by ratio of
509+ // target/actual frame time. Only modify chunk size if our measurement
510+ // reflects the cost of a creating a full chunk's worth of instances; this
511+ // avoids scaling up the chunk size if we e.g. quickly re-rendered instances
512+ // in place
513+ if ( this . __shouldMeasureChunk ) {
514+ const renderTime = performance . now ( ) - this . __renderStartTime ;
515+ const ratio = this . _targetFrameTime / renderTime ;
516+ this . __chunkCount = Math . round ( this . __chunkCount * ratio ) || 1 ;
517+ }
518+ // Enqueue a new render if we haven't reached the full size of the list
519+ if ( this . __shouldContinueChunking ) {
520+ this . __debounceRender ( this . __render ) ;
521+ }
478522 }
479523
480524 __observeChanged ( ) {
@@ -491,7 +535,9 @@ export class DomRepeat extends domRepeatBase {
491535 if ( ! this . __handleItemPath ( change . path , change . value ) ) {
492536 // Otherwise, the array was reset ('items') or spliced ('items.splices'),
493537 // so queue a full refresh
494- this . __initializeChunking ( ) ;
538+ if ( change . path === 'items' && ! this . reuseChunkedInstances ) {
539+ this . __restartChunking = true ;
540+ }
495541 this . __debounceRender ( this . __render ) ;
496542 }
497543 }
@@ -561,8 +607,6 @@ export class DomRepeat extends domRepeatBase {
561607 composed : true
562608 } ) ) ;
563609 }
564- // Check to see if we need to render more items
565- this . __tryRenderChunk ( ) ;
566610 }
567611
568612 __applyFullRefresh ( ) {
@@ -580,6 +624,11 @@ export class DomRepeat extends domRepeatBase {
580624 if ( this . __sortFn ) {
581625 isntIdxToItemsIdx . sort ( ( a , b ) => this . __sortFn ( items [ a ] , items [ b ] ) ) ;
582626 }
627+ // If we're chunking, increase the limit if there are new instances to
628+ // create and schedule the next chunk
629+ if ( this . initialCount ) {
630+ this . __updateLimitAndScheduleNextChunk ( isntIdxToItemsIdx . length ) ;
631+ }
583632 // items->inst map kept for item path forwarding
584633 const itemsIdxToInstIdx = this . __itemsIdxToInstIdx = { } ;
585634 let instIdx = 0 ;
0 commit comments