web: performance!

Instead of actively updating the timeline we render all elements into a
map. When there are new events to render a button shows up at the top
allowing the user to manually view them.

This needs a fix to view more down the page (I set it to 50000 for view
switchign) and I need to fix the initial page view.
This commit is contained in:
Thomas Mathews 2022-12-20 16:55:25 -08:00
parent 5bcb63973c
commit ca7abdd0b6
6 changed files with 124 additions and 73 deletions

View file

@ -138,6 +138,7 @@ button.nav > img.icon {
top: 0; top: 0;
z-index: var(--zHeader); z-index: var(--zHeader);
backdrop-filter: blur(20px); backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
} }
#view header > label { #view header > label {
padding: 15px; padding: 15px;
@ -185,6 +186,18 @@ button.nav > img.icon {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
.show-new {
text-align: center;
}
.show-new > button {
color: var(--clrText);
border: none;
padding: 15px;
background: transparent;
width: 100%;
font-size: var(--fsNormal);
font-weight: bold;
}
.userpic { /* TODO remove .userpic and use helper class */ .userpic { /* TODO remove .userpic and use helper class */
flex-shrink: 1; flex-shrink: 1;
} }

View file

@ -132,6 +132,10 @@
<img class="dark-invert" src="icon/loader-fragment.svg"/> <img class="dark-invert" src="icon/loader-fragment.svg"/>
</div> </div>
</div> </div>
<div id="show-new" class="show-new bottom-border hide">
<button onclick="show_new()">
Show New (<span role="count">0</span>)</button>
</div>
<div id="timeline" class="events"></div> <div id="timeline" class="events"></div>
</div> </div>
</div> </div>

View file

@ -81,7 +81,7 @@ function event_is_spam(ev, contacts, pow) {
return ev.pow >= pow return ev.pow >= pow
} }
function event_cmp_created(a, b) { function event_cmp_created(a={}, b={}) {
if (a.created_at > b.created_at) if (a.created_at > b.created_at)
return 1; return 1;
if (a.created_at < b.created_at) if (a.created_at < b.created_at)

View file

@ -41,7 +41,7 @@ function model_process_event(model, ev) {
if (model_event_has_unknown_ids(model, ev)) if (model_event_has_unknown_ids(model, ev))
schedule_unknown_refetch(model); schedule_unknown_refetch(model);
// Refresh timeline // Queue event for rendering
model.invalidated.push(ev.id); model.invalidated.push(ev.id);
} }
@ -250,5 +250,6 @@ function new_model() {
friend_of_friends: new Set(), friend_of_friends: new Set(),
}, },
invalidated: [], // event ids which should be added/removed invalidated: [], // event ids which should be added/removed
elements: {},
}; };
} }

View file

@ -42,13 +42,28 @@ function view_timeline_apply_mode(model, mode, opts={}) {
find_node("#newpost").classList.toggle("hide", mode != VM_FRIENDS); find_node("#newpost").classList.toggle("hide", mode != VM_FRIENDS);
find_node("#timeline").classList.toggle("reverse", mode == VM_THREAD); find_node("#timeline").classList.toggle("reverse", mode == VM_THREAD);
// Show or hide all applicable events related to the mode. // Remove all
xs = el.querySelectorAll(".event"); // This is faster than removing one by one
for (const x of xs) { el.innerHTML = "";
let evid = x.id.substr(2); // Build DOM fragment and render it
let ev = model.all_events[evid]; let count = 0;
x.classList.toggle("hide", const evs = model_events_arr(model)
!view_mode_contains_event(model, ev, mode, opts)); const fragment = new DocumentFragment();
for (let i = evs.length - 1; i >= 0 && count < 50000; i--) {
const ev = evs[i];
if (!view_mode_contains_event(model, ev, mode, opts))
continue;
let ev_el = model.elements[ev.id];
if (!ev_el)
continue;
fragment.appendChild(ev_el);
count++;
}
if (count > 0) {
find_node("#view .loading-events").classList.add("hide");
el.append(fragment);
view_set_show_count(0);
view_timeline_update_timestamps();
} }
return mode; return mode;
@ -65,19 +80,15 @@ function view_timeline_update(model) {
pubkey: el.dataset.pubkey, pubkey: el.dataset.pubkey,
}; };
// for each event not rendered, go through the list and render it marking let count = 0;
// it as rendered and adding it to the appropriate fragment. fragments are const latest_ev = el.firstChild ?
// created based on slices in the existing timeline. fragments are started model.all_events[el.firstChild.id.slice(2)] : undefined;
// at the previous event
// const fragments = {};
// const cache = {};
// Dumb function to insert needed events
let visible_count = 0;
const all = model_events_arr(model); const all = model_events_arr(model);
const left_overs = []; const left_overs = [];
while (model.invalidated.length > 0) { while (model.invalidated.length > 0) {
var evid = model.invalidated.pop(); var evid = model.invalidated.pop();
if (model.elements[evid])
continue;
var ev = model.all_events[evid]; var ev = model.all_events[evid];
if (!event_is_renderable(ev) || model_is_event_deleted(model, evid)) { if (!event_is_renderable(ev) || model_is_event_deleted(model, evid)) {
let x = find_node("#ev"+evid, el); let x = find_node("#ev"+evid, el);
@ -85,46 +96,71 @@ function view_timeline_update(model) {
continue; continue;
} }
// If event is in el already, do nothing or update? const html = render_event(model, ev, {});
let ev_el = find_node("#ev"+evid, el); // Put it back on the stack to re-render if it's not ready.
if (ev_el) { if (html == "") {
continue;
} else {
const html = render_event(model, ev, {});
// Put it back on the stack to re-render if it's not ready.
if (html == "") {
left_overs.push(evid);
continue;
}
const div = document.createElement("div");
div.innerHTML = html;
ev_el = div.firstChild;
if (!view_mode_contains_event(model, ev, mode, opts)) {
ev_el.classList.add("hide");
} else {
visible_count++;
}
}
// find prior event element and insert it before that
let prior_el;
let prior_idx = arr_bsearch_insert(all, ev, event_cmp_created);
while (prior_idx >= 0 && !prior_el) {
prior_el = find_node("#ev"+all[prior_idx].id, el);
prior_idx--;
}
if (prior_el) {
el.insertBefore(ev_el, prior_el);
} else if (el.childElementCount == 0) {
el.appendChild(ev_el);
} else {
left_overs.push(evid); left_overs.push(evid);
continue;
}
const div = document.createElement("div");
div.innerHTML = html;
ev_el = div.firstChild;
model.elements[evid] = ev_el;
// If the new element is newer than the latest & is viewable then
// we want to increase the count of how many to add to view
if (event_cmp_created(ev, latest_ev) >= 0 && view_mode_contains_event(model, ev, mode, opts)) {
count++;
} }
} }
model.invalidated = model.invalidated.concat(left_overs); model.invalidated = model.invalidated.concat(left_overs);
if (visible_count > 0) if (count > 0) {
view_set_show_count(count, true);
}
}
function view_set_show_count(count, add) {
const show_el = find_node("#show-new")
const num_el = find_node("#show-new span", show_el);
if (add) {
count += parseInt(num_el.innerText || 0)
}
num_el.innerText = count;
show_el.classList.toggle("hide", count <= 0);
}
function view_timeline_show_new(model) {
const el = view_get_timeline_el();
const mode = el.dataset.mode;
const opts = {
thread_id: el.dataset.threadId,
pubkey: el.dataset.pubkey,
};
const latest_evid = el.firstChild ? el.firstChild.id.slice(2) : undefined;
let count = 0;
const evs = model_events_arr(model)
const fragment = new DocumentFragment();
for (let i = evs.length - 1; i >= 0 && count < 500; i--) {
const ev = evs[i];
if (latest_evid && ev.id == latest_evid) {
break;
}
if (!view_mode_contains_event(model, ev, mode, opts))
continue;
let ev_el = model.elements[ev.id];
if (!ev_el)
continue;
fragment.appendChild(ev_el);
count++;
}
if (count > 0) {
find_node("#view .loading-events").classList.add("hide"); find_node("#view .loading-events").classList.add("hide");
el.prepend(fragment);
}
view_set_show_count(-count, true);
view_timeline_update_timestamps();
} }
function view_timeline_update_profiles(model, ev) { function view_timeline_update_profiles(model, ev) {
@ -138,22 +174,19 @@ function view_timeline_update_profiles(model, ev) {
redraw_my_pfp(model); redraw_my_pfp(model);
} }
// Update displayed names const name = fmt_profile_name(p, fmt_pubkey(pk));
xs = el.querySelectorAll(`.username[data-pubkey='${pk}']`) const pic = get_picture(pk, p)
html = fmt_profile_name(p, fmt_pubkey(pk)); for (const evid in model.elements) {
for (const x of xs) { if (model.all_events[evid].pubkey != pk)
x.innerText = html; continue;
} const el = model.elements[evid];
find_node(`.username[data-pubkey='${pk}']`, el).innerText = name;
// Update profile pictures find_node(`img.pfp[data-pubkey='${pk}']`, el).src = pic;
xs = el.querySelectorAll(`img.pfp[data-pubkey='${pk}']`);
html = get_picture(pk, p)
for (const x of xs) {
x.src = html;
} }
} }
function view_timeline_update_timestamps(model) { function view_timeline_update_timestamps() {
// TODO only update elements that are fresh and are in DOM
const el = view_get_timeline_el(); const el = view_get_timeline_el();
let xs = el.querySelectorAll(".timestamp"); let xs = el.querySelectorAll(".timestamp");
let now = new Date().getTime(); let now = new Date().getTime();
@ -166,16 +199,12 @@ function view_timeline_update_timestamps(model) {
function view_timeline_update_reaction(model, ev) { function view_timeline_update_reaction(model, ev) {
let el; let el;
const o = event_parse_reaction(ev); const o = event_parse_reaction(ev);
if (!o) { if (!o)
return; return;
}
const ev_id = o.e; const ev_id = o.e;
const root = find_node(`#ev${ev_id}`); const root = model.elements[ev_id];
if (!root) { if (!root)
// It's possible the event didn't get rendered yet from the
// invalidation stack. In which case emojis will get rendered then.
return; return;
}
// Update reaction groups // Update reaction groups
el = find_node(`.reactions`, root); el = find_node(`.reactions`, root);

View file

@ -115,6 +115,10 @@ function click_toggle_follow_user(el) {
contacts_save(contacts); contacts_save(contacts);
} }
function show_new() {
view_timeline_show_new(DAMUS);
}
/* click_event opens the thread view from the element's specified element id /* click_event opens the thread view from the element's specified element id
* "dataset.eid". * "dataset.eid".
*/ */