diff --git a/src/lib/template/dom-repeat.html b/src/lib/template/dom-repeat.html
index 0a56ed7891..b75f8ed496 100644
--- a/src/lib/template/dom-repeat.html
+++ b/src/lib/template/dom-repeat.html
@@ -188,7 +188,75 @@
* This is useful in rate-limiting shuffing of the view when
* item changes may be frequent.
*/
- delay: Number
+ delay: Number,
+
+ /**
+ * When `limit` is defined, the number of actually rendered template
+ * instances will be limited to this count.
+ *
+ * Note that if `initialCount` is used, the `limit` property will be
+ * automatically controlled and should not be set by the user.
+ */
+ limit: {
+ value: Infinity,
+ type: Number,
+ observer: '_limitChanged'
+ },
+
+ /**
+ * Defines an initial count of template instances to render after setting
+ * the `items` array, before the next paint, and puts the `dom-repeat`
+ * into "chunking mode". The remaining items will be created and rendered
+ * incrementally at each animation frame therof until all instnaces have
+ * been rendered. The number of instances created each animation frame
+ * can be controlled via the `chunkCount` property.
+ */
+ initialCount: {
+ type: Number,
+ value: 0
+ },
+
+ /**
+ * When `initialCount` is used, defines the number of instances to be
+ * created at each animation frame after rendering the `initialCount`.
+ * When left to the default `'auto'` value, the chunk count will be
+ * throttled automatically using a best effort scheme to maintain the
+ * value of the `targetFramerate` property.
+ */
+ chunkCount: {
+ type: Number,
+ value: 'auto'
+ },
+
+ /**
+ * When `initialCount` is used and `chunkCount` is set to `'auto'`, this
+ * property defines a frame rate to target by throttling the number of
+ * instances rendered each frame to not exceed the budget for the target
+ * frame rate. Setting this to a higher number will allow lower latency
+ * and higher throughput for things like event handlers, but will result
+ * in a longer time for the remaining items to complete rendering.
+ */
+ targetFramerate: {
+ type: Number,
+ value: 20
+ },
+
+ /**
+ * Maximum number of removed instances to pool for reuse when rows are
+ * added in a future turn. By default, pooling is enabled.
+ *
+ * Set to 0 to disable pooling, which will allow all removed instances to
+ * be garbage collected.
+ */
+ poolSize: {
+ type: Number,
+ value: 1000
+ },
+
+ _targetFrameTime: {
+ computed: '_computeFrameTime(targetFramerate)'
+ }
+
},
behaviors: [
@@ -196,23 +264,32 @@
],
observers: [
- '_itemsChanged(items.*)'
+ '_itemsChanged(items.*)',
+ '_initializeChunkCount(initialCount, chunkCount)'
],
created: function() {
this._instances = [];
+ this._pool = [];
+ this._boundRenderChunk = this._renderChunk.bind(this);
},
detached: function() {
for (var i=0; i= 0 && this._chunkCount &&
+ Math.min(this.limit, this._instances.length) < this.items.length) {
+ this.debounce('renderChunk', this._requestRenderChunk);
+ }
+ },
+
+ _requestRenderChunk: function() {
+ requestAnimationFrame(this._boundRenderChunk);
+ },
+
+ _renderChunk: function() {
+ if (this.chunkCount == 'auto') {
+ // Simple auto chunkSize throttling algorithm based on feedback loop:
+ // measure actual time between frames and scale chunk count by ratio
+ // of target/actual frame time
+ var prevChunkTime = this._currChunkTime;
+ this._currChunkTime = performance.now();
+ var chunkTime = this._currChunkTime - prevChunkTime;
+ if (chunkTime) {
+ var ratio = this._targetFrameTime / chunkTime;
+ this._chunkCount = Math.round(this._chunkCount * ratio) || 1;
+ }
+ }
+ this.limit += this._chunkCount;
+ },
+
_observeChanged: function() {
this._observePaths = this.observe &&
this.observe.replace('.*', '.').split(' ');
@@ -324,7 +443,7 @@
if (this._needFullRefresh) {
this._applyFullRefresh();
this._needFullRefresh = false;
- } else {
+ } else if (this._keySplices.length) {
if (this._sortFn) {
this._applySplicesUserSort(this._keySplices);
} else {
@@ -338,14 +457,28 @@
}
this._keySplices = [];
this._indexSplices = [];
- // Update final _keyToInstIdx and instance indices
+ // Update final _keyToInstIdx, instance indices, and replace placeholders
var keyToIdx = this._keyToInstIdx = {};
- for (var i=0; i=0; i--) {
var inst = this._instances[i];
+ if (inst.isPlaceholder && i=this.limit) {
+ inst = this._insertRow(i, inst.__key__, true, true);
+ }
keyToIdx[inst.__key__] = i;
- inst.__setProperty(this.indexAs, i, true);
+ if (!inst.isPlaceholder) {
+ inst.__setProperty(this.indexAs, i, true);
+ }
}
+ // Reset the pool
+ // TODO(kschaaf): Allow pool to be reused across turns & between nested
+ // peer repeats (requires updating parentProps when reusing from pool)
+ this._pool.length = 0;
+ // Notify users
this.fire('dom-change');
+ // Check to see if we need to render more items
+ this._tryRenderChunk();
},
// Render method 1: full refesh
@@ -385,17 +518,20 @@
var key = keys[i];
var inst = this._instances[i];
if (inst) {
- inst.__setProperty('__key__', key, true);
- inst.__setProperty(this.as, c.getItem(key), true);
+ if (inst.isPlaceholder) {
+ inst.__key__ = key;
+ } else {
+ inst.__setProperty('__key__', key, true);
+ inst.__setProperty(this.as, c.getItem(key), true);
+ }
} else {
- this._instances.push(this._insertRow(i, key));
+ this._insertRow(i, key);
}
}
// Remove any extra instances from previous state
- for (; i=i; j--) {
+ this._detachRow(j);
}
- this._instances.splice(keys.length, this._instances.length-keys.length);
},
_keySort: function(a, b) {
@@ -414,7 +550,6 @@
var c = this.collection;
var instances = this._instances;
var keyMap = {};
- var pool = [];
var sortFn = this._sortFn || this._keySort.bind(this);
// Dedupe added and removed keys to a final added/removed map
splices.forEach(function(s) {
@@ -448,8 +583,7 @@
var idx = removedIdxs[i];
// Removed idx may be undefined if item was previously filtered out
if (idx !== undefined) {
- pool.push(this._detachRow(idx));
- instances.splice(idx, 1);
+ this._detachRow(idx);
}
}
}
@@ -468,12 +602,12 @@
// Insertion-sort new instances into place (from pool or newly created)
var start = 0;
for (var i=0; i=0; i--) {
- var inst = this._instances[i];
- if (inst.isPlaceholder) {
- this._instances[i] = this._insertRow(i, inst.key, pool, true);
- }
- }
},
- _detachRow: function(idx) {
+ _detachRow: function(idx, keepInstance) {
var inst = this._instances[idx];
if (!inst.isPlaceholder) {
var parentNode = Polymer.dom(this).parentNode;
@@ -545,22 +661,41 @@
var el = inst._children[i];
Polymer.dom(inst.root).appendChild(el);
}
+ this._pool.push(inst);
+ }
+ if (!keepInstance) {
+ this._instances.splice(idx, 1);
}
return inst;
},
- _insertRow: function(idx, key, pool, replace) {
+ _insertRow: function(idx, key, replace, makePlaceholder) {
var inst;
- if (inst = pool && pool.pop()) {
- inst.__setProperty(this.as, this.collection.getItem(key), true);
- inst.__setProperty('__key__', key, true);
+ if (makePlaceholder || idx >= this.limit) {
+ inst = {
+ isPlaceholder: true,
+ __key__: key
+ };
} else {
- inst = this._generateRow(idx, key);
+ if (inst = this._pool.pop()) {
+ inst.__setProperty(this.as, this.collection.getItem(key), true);
+ inst.__setProperty('__key__', key, true);
+ } else {
+ inst = this._generateRow(idx, key);
+ }
+ var beforeRow = this._instances[replace ? idx + 1 : idx];
+ var beforeNode = beforeRow && !beforeRow.isPlaceholder ? beforeRow._children[0] : this;
+ var parentNode = Polymer.dom(this).parentNode;
+ Polymer.dom(parentNode).insertBefore(inst.root, beforeNode);
+ }
+ if (replace) {
+ if (makePlaceholder) {
+ this._detachRow(idx, true);
+ }
+ this._instances[idx] = inst;
+ } else {
+ this._instances.splice(idx, 0, inst);
}
- var beforeRow = this._instances[replace ? idx + 1 : idx];
- var beforeNode = beforeRow ? beforeRow._children[0] : this;
- var parentNode = Polymer.dom(this).parentNode;
- Polymer.dom(parentNode).insertBefore(inst.root, beforeNode);
return inst;
},