From fddf2892cde66336453505ded0fecaac464b3ad1 Mon Sep 17 00:00:00 2001 From: antelle Date: Sat, 20 Mar 2021 19:17:57 +0100 Subject: [PATCH] fix #1580: high memory usage on large files, virtual scrolling of entries --- app/scripts/framework/views/view.js | 2 +- app/scripts/views/app-view.js | 3 +- app/scripts/views/list-view.js | 132 +++++++++++++++++++++++++--- app/styles/areas/_list.scss | 32 +++++-- app/templates/list-item-table.hbs | 22 ++--- app/templates/list-mode-list.hbs | 3 + app/templates/list-mode-table.hbs | 11 +++ app/templates/list-table.hbs | 13 --- 8 files changed, 171 insertions(+), 47 deletions(-) create mode 100644 app/templates/list-mode-list.hbs create mode 100644 app/templates/list-mode-table.hbs delete mode 100644 app/templates/list-table.hbs diff --git a/app/scripts/framework/views/view.js b/app/scripts/framework/views/view.js index 529fa866..f3e7fe48 100644 --- a/app/scripts/framework/views/view.js +++ b/app/scripts/framework/views/view.js @@ -251,7 +251,6 @@ class View extends EventEmitter { FocusManager.setModal(null); } } - this.emit(visible ? 'show' : 'hide'); if (this.el) { this.el.classList.toggle('show', !!visible); this.el.classList.toggle('hide', !visible); @@ -259,6 +258,7 @@ class View extends EventEmitter { Tip.hideTips(this.el); } } + this.emit(visible ? 'show' : 'hide'); } isHidden() { diff --git a/app/scripts/views/app-view.js b/app/scripts/views/app-view.js index a04f9dfd..6dcb231e 100644 --- a/app/scripts/views/app-view.js +++ b/app/scripts/views/app-view.js @@ -202,7 +202,6 @@ class AppView extends View { this.views.menu.show(); this.views.menuDrag.$el.parent().show(); this.views.listWrap.show(); - this.views.list.show(); this.views.listDrag.show(); this.views.details.show(); this.views.footer.show(); @@ -211,6 +210,8 @@ class AppView extends View { this.hideSettings(); this.hideKeyChange(); this.hideImportCsv(); + + this.views.list.show(); } hideOpenFile() { diff --git a/app/scripts/views/list-view.js b/app/scripts/views/list-view.js index a52bc85a..1cabffa1 100644 --- a/app/scripts/views/list-view.js +++ b/app/scripts/views/list-view.js @@ -68,12 +68,20 @@ class ListView extends View { this.readTableColumnsEnabled(); this.items = new SearchResultCollection(); + this.renderedItems = new Map(); } render() { + if (!this.isVisible()) { + this.pendingRender = true; + return; + } + this.pendingRender = false; + if (!this.itemsEl) { super.render(); this.itemsEl = this.$el.find('.list__items>.scroller'); + this.itemsEl.on('scroll', () => this.renderVisibleItems()); this.views.search.render(); this.setTableView(); @@ -84,9 +92,9 @@ class ListView extends View { }); } if (this.items.length) { - const itemTemplate = this.getItemTemplate(); const itemsTemplate = this.getItemsTemplate(); const noColor = AppSettingsModel.colorfulIcons ? '' : 'grayscale'; + const presenter = new EntryPresenter( this.getDescField(), noColor, @@ -99,16 +107,28 @@ class ListView extends View { } }); presenter.columns = columns; - let itemsHtml = ''; - this.items.forEach((item) => { - presenter.present(item); - itemsHtml += itemTemplate(presenter, DefaultTemplateOptions); - }, this); + this.presenter = presenter; + + presenter.present(this.items[0]); + const itemTemplate = this.getItemTemplate(); + const itemsHtml = itemTemplate(presenter, DefaultTemplateOptions); + const html = itemsTemplate( { itemsHtml, columns: this.tableColumns }, DefaultTemplateOptions ); this.itemsEl.html(html); + this.itemsContainerEl = this.itemsEl.find('.list__items-container:first')[0]; + + const firstListItem = this.itemsContainerEl.firstElementChild; + this.itemHeight = firstListItem.getBoundingClientRect().height; + + this.renderedItems = new Map([[0, firstListItem]]); + + const totalHeight = this.itemHeight * this.items.length; + this.itemsContainerEl.style.minHeight = totalHeight + 'px'; + + this.renderVisibleItems(); } else { this.itemsEl.html(this.emptyTemplate({}, DefaultTemplateOptions)); } @@ -117,16 +137,12 @@ class ListView extends View { getItemsTemplate() { if (this.model.settings.tableView) { - return require('templates/list-table.hbs'); + return require('templates/list-mode-table.hbs'); } else { - return this.renderPlainItems; + return require('templates/list-mode-list.hbs'); } } - renderPlainItems(data) { - return data.itemsHtml; - } - getItemTemplate() { if (this.model.settings.tableView) { return require('templates/list-item-table.hbs'); @@ -135,6 +151,88 @@ class ListView extends View { } } + renderVisibleItems() { + if (!this.isVisible()) { + return; + } + + const scrollEl = this.itemsEl[0]; + const rect = scrollEl.getBoundingClientRect(); + + const pxTop = scrollEl.scrollTop; + const pxHeight = rect.height; + const itemHeight = this.itemHeight; + const renderedItems = this.renderedItems; + + let firstIx = Math.max(0, Math.floor(pxTop / itemHeight)); + let lastIx = Math.min(this.items.length - 1, Math.ceil((pxTop + pxHeight) / itemHeight)); + + const visibleCount = lastIx - firstIx; + firstIx = Math.max(0, firstIx - visibleCount); + lastIx = Math.min(this.items.length - 1, lastIx + visibleCount); + + const itemTemplate = this.getItemTemplate(); + const presenter = this.presenter; + + let itemsHtml = ''; + const renderedIndices = []; + + for (let ix = firstIx; ix <= lastIx; ix++) { + const item = this.items[ix]; + if (renderedItems.has(ix)) { + continue; + } + presenter.present(item); + itemsHtml += itemTemplate(presenter, DefaultTemplateOptions); + renderedIndices.push(ix); + } + + const tempEl = document.createElement('div'); + tempEl.innerHTML = itemsHtml; + const renderedElements = [...tempEl.children]; + + for (let i = 0; i < renderedElements.length; i++) { + const el = renderedElements[i]; + const ix = renderedIndices[i]; + this.itemsContainerEl.append(el); + el.style.top = ix * itemHeight + 'px'; + renderedItems.set(ix, el); + } + + const maxRenderedItems = visibleCount * 5; + + if (renderedItems.size > maxRenderedItems) { + for (const [ix, el] of this.renderedItems) { + if (ix < firstIx || ix > lastIx) { + el.remove(); + renderedItems.delete(ix); + } + } + } + } + + ensureItemRendered(ix) { + if (this.renderedItems.has(ix)) { + return; + } + + const item = this.items[ix]; + const itemTemplate = this.getItemTemplate(); + + this.presenter.present(item); + const itemHtml = itemTemplate(this.presenter, DefaultTemplateOptions); + + const tempEl = document.createElement('div'); + tempEl.innerHTML = itemHtml; + + const [el] = tempEl.children; + + this.itemsContainerEl.append(el); + el.style.top = ix * this.itemHeight + 'px'; + + this.renderedItems.set(ix, el); + } + getDescField() { return this.model.sort.replace('-', ''); } @@ -203,7 +301,12 @@ class ListView extends View { } selectItem(item) { + this.presenter.activeEntryId = item.id; this.model.activeEntryId = item.id; + + const ix = this.items.indexOf(item); + this.ensureItemRendered(ix); + Events.emit('entry-selected', item); this.itemsEl.find('.list__item--active').removeClass('list__item--active'); const itemEl = document.getElementById(item.id); @@ -220,6 +323,9 @@ class ListView extends View { viewShown() { this.views.search.show(); + if (this.pendingRender) { + this.render(); + } } viewHidden() { @@ -248,6 +354,7 @@ class ListView extends View { viewResized(size) { this.setSize(size); this.throttleSetViewSizeSetting(size); + this.renderVisibleItems(); } throttleSetViewSizeSetting = throttle((size) => { @@ -256,6 +363,7 @@ class ListView extends View { filterChanged(filter) { this.items = filter.entries; + this.renderedItems = new Map(); this.render(); } diff --git a/app/styles/areas/_list.scss b/app/styles/areas/_list.scss index 72d375aa..a832d725 100644 --- a/app/styles/areas/_list.scss +++ b/app/styles/areas/_list.scss @@ -119,20 +119,29 @@ } &__table { - border-collapse: collapse; - width: calc(100% - 2px); - td, - th { + &-body { + position: relative; + } + &-row { + display: flex; + align-items: center; + width: 100%; + } + &-cell { padding: $base-padding; text-align: left; + flex-grow: 1; + flex-shrink: 0; + flex-basis: 0; &:first-child { text-align: center; + padding: 0; + width: 3em; + flex-basis: 3em; + flex-grow: 0; + flex-shrink: 0; } } - th:first-child { - padding: 0; - width: 3em; - } &-options { @include icon-btn(); cursor: pointer; @@ -140,10 +149,13 @@ } &__item { - padding: $base-padding; + left: 0; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; + position: absolute; + top: 0; + box-sizing: border-box; @include nomobile { @include area-selectable(); &--active, @@ -163,7 +175,9 @@ @include nomobile { border-radius: var(--block-border-radius); } + padding: $base-padding; margin: 0 $small-spacing; + width: calc(100% - #{$small-spacing * 2}); } &--expired { diff --git a/app/templates/list-item-table.hbs b/app/templates/list-item-table.hbs index 92f5f79a..71e19cd1 100644 --- a/app/templates/list-item-table.hbs +++ b/app/templates/list-item-table.hbs @@ -1,16 +1,16 @@ - - +
+
{{~#if customIcon~}} {{~else~}} {{~/if~}} - - {{#if columns.title}}{{#if title}}{{title}}{{else}}({{res 'noTitle'}}){{/if}}{{/if}} - {{#if columns.user}}{{user}}{{/if}} - {{#if columns.url}}{{url}}{{/if}} - {{#if columns.tags}}{{tags}}{{/if}} - {{#if columns.notes}}{{notes}}{{/if}} - {{#if columns.groupName}}{{groupName}}{{/if}} - {{#if columns.fileName}}{{fileName}}{{/if}} - +
+ {{#if columns.title}}
{{#if title}}{{title}}{{else}}({{res 'noTitle'}}){{/if}}
{{/if}} + {{#if columns.user}}
{{user}}
{{/if}} + {{#if columns.url}}
{{url}}
{{/if}} + {{#if columns.tags}}
{{tags}}
{{/if}} + {{#if columns.notes}}
{{notes}}
{{/if}} + {{#if columns.groupName}}
{{groupName}}
{{/if}} + {{#if columns.fileName}}
{{fileName}}
{{/if}} +
diff --git a/app/templates/list-mode-list.hbs b/app/templates/list-mode-list.hbs new file mode 100644 index 00000000..7d034852 --- /dev/null +++ b/app/templates/list-mode-list.hbs @@ -0,0 +1,3 @@ +
+ {{{itemsHtml}}} +
diff --git a/app/templates/list-mode-table.hbs b/app/templates/list-mode-table.hbs new file mode 100644 index 00000000..dc6c62cd --- /dev/null +++ b/app/templates/list-mode-table.hbs @@ -0,0 +1,11 @@ +
+
+
+ {{#each columns as |col|}} + {{#if col.enabled}}
{{Res col.name}}
{{/if}} + {{/each}} +
+
+ {{{itemsHtml}}} +
+
diff --git a/app/templates/list-table.hbs b/app/templates/list-table.hbs deleted file mode 100644 index 1741d462..00000000 --- a/app/templates/list-table.hbs +++ /dev/null @@ -1,13 +0,0 @@ - - - - - {{#each columns as |col|}} - {{#if col.enabled}}{{/if}} - {{/each}} - - - - {{{itemsHtml}}} - -
{{Res col.name}}