Skip to content

Commit b40840b

Browse files
committed
Fixes for several related dom-repeat chunking issues. Fixes #5631.
* Only restart chunking (resetting the list to the initialCount) if the `items` array itself changed (and not splices to the array), to match Polymer 1 behavior. * Add `reuseChunkedInstances` option to allow reusing instances even when `items` changes; this is likely the more common optimal case when using immutable data, but making it optional for backward compatibility. * Only measure render time and throttle the chunk size if we rendered a full chunk of new items. Ensures that fast re-renders of existing items don't cause the chunk size to scale up dramatically, subsequently causing too many new items to be created in one chunk. * Increase the limit by the chunk size as part of any render if there are new items to render, rather than only as a result of rendering. * Continue chunking by comparing the filtered item count to the limit (not the unfiltered item count).
1 parent c5c7eec commit b40840b

File tree

3 files changed

+289
-208
lines changed

3 files changed

+289
-208
lines changed

lib/elements/dom-repeat.js

Lines changed: 80 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -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;

test/unit/dom-repeat-elements.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ Polymer({
434434
});
435435
Polymer({
436436
_template: html`
437-
<template id="repeater" is="dom-repeat" items="{{items}}" initial-count="10">
437+
<template id="repeater" is="dom-repeat" items="{{items}}" initial-count="10" target-framerate="25">
438438
<x-wait>{{item.prop}}</x-wait>
439439
</template>
440440
`,

0 commit comments

Comments
 (0)