A page used to be requested at the moment the reader hit the bottom, so the cards that appeared were empty frames filling in as their thumbnails arrived. Now the next page is fetched as soon as the current one renders and its thumbnails are decoded off-screen (low priority, started on an idle callback, so warming never competes with what's on screen), then held until the second-to-last row comes into view -- at which point the cards appear already finished, and the page after that starts loading. The two loaders are split into fetch-a-batch and commit-a-batch so the prefetcher and the on-demand path share them; a held batch is dropped when the result set changes (search, channel, filters). Three things this surfaced, all handled: loadVideos awaited the in-flight prefetch before raising state.isLoading, so every caller that arrived meanwhile sailed past the guard and started a duplicate page load, each one re-filling the viewport and calling back in. On a short desktop page that amplified until the tab stopped responding. A guard now covers the whole call. The reveal test can't be "the topmost visible card is in the last two rows": a desktop viewport shows several rows at once, so the reader would reach the end of the list without it ever passing. It's now "the second-to-last row has come into view", measured against the layout. The reels feed pulls pages through the same entry point, where the grid's scroll position means nothing -- it (and the Load more button) now pass force, which skips the hold. Verified in headless Chrome: page 2 fetched and all 12 of its thumbnails warmed while it was still hidden, grid still at 12 cards; revealed on the second-to-last row (phone: viewport bottom 4337px vs row at 4335px; desktop 4-col: 1704px vs 1687px) with its images already decoded; feed paging, search reset, rotation, momentum and playback all still good. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QBDkEXP4htyXTCZUwMLphd
56 lines
2.0 KiB
JavaScript
56 lines
2.0 KiB
JavaScript
window.App = window.App || {};
|
|
|
|
(function() {
|
|
// App bootstrap: initialize storage, render UI, and load the first page.
|
|
async function initApp() {
|
|
await App.storage.ensureDefaults();
|
|
App.ui.applyTheme();
|
|
App.ui.applyPreferredQuality();
|
|
App.ui.applyFeedEndBehavior();
|
|
App.ui.applyDensity();
|
|
// Set the text-size CSS variable before the first pack so initial card
|
|
// heights are measured at the user's chosen size.
|
|
document.documentElement.style.setProperty('--card-font-scale', App.storage.getFontScale());
|
|
App.ui.renderMenu();
|
|
App.favorites.renderBar();
|
|
App.ui.bindGlobalHandlers();
|
|
|
|
App.videos.observeSentinel();
|
|
|
|
const loadMoreBtn = document.getElementById('load-more-btn');
|
|
if (loadMoreBtn) {
|
|
loadMoreBtn.onclick = () => {
|
|
App.videos.loadVideos({ force: true });
|
|
};
|
|
}
|
|
|
|
const errorToastClose = document.getElementById('error-toast-close');
|
|
if (errorToastClose) {
|
|
errorToastClose.onclick = () => {
|
|
const toast = document.getElementById('error-toast');
|
|
if (toast) toast.classList.remove('show');
|
|
};
|
|
}
|
|
|
|
window.addEventListener('resize', () => {
|
|
App.videos.ensureViewportFilled();
|
|
});
|
|
|
|
await App.videos.loadVideos();
|
|
App.favorites.syncButtons();
|
|
|
|
// The UI above is rendered entirely from the last known status cached in
|
|
// localStorage, so startup never blocks on (or breaks because of) a slow
|
|
// or failing status endpoint. Now fetch fresh status in the background and
|
|
// reconcile the UI with whatever comes back.
|
|
App.storage.refreshServerStatusInBackground();
|
|
|
|
// Watch for frontend deploys and seamlessly reload/hot-swap changed assets.
|
|
if (App.version && App.version.start) {
|
|
App.version.start();
|
|
}
|
|
}
|
|
|
|
initApp();
|
|
})();
|