Documentation Index
Fetch the complete documentation index at: https://docs.discovr.media/llms.txt
Use this file to discover all available pages before exploring further.
You’ve built navigation and profiles. Now make your app feel fast. This guide walks through the loading patterns that keep Discovr apps responsive.
The core idea is simple: load in priority order. Hero → rows → items. Show the user something immediately, then fill in the rest as they scroll.
| Load phase | What you fetch | When |
|---|
| Immediate | getPage() hero | On page open |
| After hero renders | getRows() row shells | After hero visible |
| On scroll | getRowItems() for one row | When row enters viewport |
| Pagination | More items / rows | When user reaches end |
This guide assumes you understand pages (hero + rows) and snapshots. If not, start with Pages Overview and Session management. Code snippets use TypeScript, Kotlin, and Swift tabs.
Page Load Strategy: Hero First
When the user opens a page, load the hero carousel immediately. Rows can come later.
- Fetch page metadata (hero) with
getPage()
- Render the hero
- Set up Intersection Observers for rows
- Fetch rows on demand as the user scrolls
async function loadPageEfficiently(pageId: string) {
// Step 1: Fetch hero (fast)
const pageData = await discovr.getPage(pageId);
renderHero(pageData.hero); // User sees hero immediately
// Step 2: Fetch rows lazily (after user sees hero)
const rows = await discovr.getRows(pageId);
// Step 3: Set up Intersection Observer for each row
rows.forEach((row) => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(async (entry) => {
if (entry.isIntersecting) {
// Row is visible; fetch its items
const items = await discovr.getRowItems(
pageId,
row.id
);
renderRow(row, items);
observer.unobserve(entry.target);
}
});
});
const rowElement = document.getElementById(`row-${row.id}`);
observer.observe(rowElement);
});
}
loadPageEfficiently(homePageId);
Metrics: Measure “Time to Hero” (how long until the hero appears). This is the user’s first impression of load speed. 1-2 seconds is typical; anything over 3 seconds feels slow.
Page load timeline:
0ms - User taps Home
200ms - Fetch getPage() (hero data)
300ms - HERO VISIBLE TO USER (KEY MILESTONE)
400ms - Fetch getRows() (row metadata)
500ms - Rows appear below hero
...
1200ms - Row 1 becomes visible
1300ms - Fetch getRowItems() for Row 1
1500ms - Row 1 items render
...
3000ms - Row 2 becomes visible
3100ms - Fetch getRowItems() for Row 2
Result: Hero in 300ms, rows by 500ms, items on demand.
Total bandwidth: only what's visible.
Rows include pagination tokens (nextPageToken, hasMore). Don’t fetch all items upfront. Fetch them as the user scrolls within the row.
async function loadRowItemsOnScroll(pageId, row) {
// Fetch the first page of items
let items = await discovr.getRowItems(pageId, row.id);
renderItems(items);
// Watch for scroll near the end
const container = document.getElementById(`row-${row.id}`);
const observer = new IntersectionObserver((entries) => {
entries.forEach(async (entry) => {
if (
entry.isIntersecting &&
row.hasMore &&
row.nextPageToken
) {
// User scrolled near the end; fetch next page
const moreItems = await discovr.getRowItems(
pageId,
row.id,
row.nextPageToken
);
items = items.concat(moreItems);
renderItems(items);
// Update pagination state
row.nextPageToken = moreItems[moreItems.length - 1]
?.nextPageToken;
row.hasMore = moreItems.length > 0;
}
});
});
observer.observe(container);
}
Why? A row might have 100 items. The user might only see the first 10 before scrolling to a different row. Why download 90 items they’ll never see?
Image Lazy Loading
Media items include image URLs. Use the browser’s native loading="lazy" attribute or an Intersection Observer to defer image fetches until the image is near the viewport.
// Native lazy loading (simplest)
function renderMediaItem(item) {
const img = document.createElement("img");
img.src = item.imageUrl;
img.loading = "lazy"; // Browser defers load until visible
img.alt = item.title;
return img;
}
// Or with Intersection Observer (more control)
function observeImageVisibility(img) {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
img.src = img.dataset.src; // Load on visibility
observer.unobserve(img);
}
});
});
observer.observe(img);
}
Why? Images are heavy. Downloading 50 poster images (10MB+) upfront kills network speed and battery life. Lazy loading fetches them only when needed.
Set a reasonable image resolution for your app. If your thumbnail is 200x300 pixels, request that size from Discovr, not the full 4K poster. This halves (or more) the image file size.
Combined Pattern: Full Home Page
Here’s a complete example tying everything together:
async function loadHomePageEfficiently(homePageId: string) {
// 1. Load hero immediately
const pageData = await discovr.getPage(homePageId);
renderHero(pageData.hero);
// 2. Insert "Continue Watching" row from profile context
const { items: continueWatching } = await discovr.getPlaybackItems({ limit: 10 });
renderContinueWatchingRow(continueWatching);
// 3. Fetch recommendation rows, but not their items yet
const rows = await discovr.getRows(homePageId);
// 4. Observe each row for visibility
rows.forEach((row) => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(async (entry) => {
if (entry.isIntersecting && !row.itemsLoaded) {
// Fetch items for visible row
const items = await discovr.getRowItems(
homePageId,
row.id
);
renderRow(row, items);
row.itemsLoaded = true;
// Also observe for pagination
observeRowEnd(row, homePageId);
}
});
});
const rowElement = document.getElementById(
`row-${row.id}`
);
observer.observe(rowElement);
});
}
async function observeRowEnd(row, pageId) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(async (entry) => {
if (
entry.isIntersecting &&
row.hasMore &&
row.nextPageToken
) {
const moreItems = await discovr.getRowItems(
pageId,
row.id,
row.nextPageToken
);
appendItems(row, moreItems);
row.nextPageToken = moreItems[moreItems.length - 1]
?.nextPageToken;
row.hasMore = moreItems.length > 0;
}
});
});
const endMarker = document.getElementById(
`row-${row.id}-end`
);
observer.observe(endMarker);
}
Profile Switching
Switching profiles creates a new session and snapshot. Don’t keep the old Home page DOM—clear it and reload. The new profile’s recommendations are completely fresh.
async function switchProfile(newProfileId: string) {
// Clear the old home page
document.getElementById("home-container").innerHTML = "";
// Select new profile (creates new session)
await discovr.selectProfile(newProfileId);
// Load new home page
const newHome = await discovr.createPage({ name: "Home" });
await loadHomePageEfficiently(newHome.id);
}
Tab Switching (Home/Movies/Series)
Each tab is a separate page. When the user taps a tab, fetch that page’s data (if not already cached).
const tabPages = {
home: null,
movies: null,
series: null,
};
async function switchTab(tab: "home" | "movies" | "series") {
// If already loaded, reuse
if (tabPages[tab]) {
renderPage(tabPages[tab]);
return;
}
// Otherwise, load from cache (getPage) doesn't require network
const pageData = await discovr.getPage(tabPageIds[tab]);
tabPages[tab] = pageData;
renderPage(pageData);
}
Navigation Drill
When the user taps a navigation row item, create a new page (which takes a moment), then load it. Show a loading indicator while fetching:
async function drillIntoGenre(genreFilter) {
showLoadingIndicator();
const newPage = await discovr.createPage({
pageFilters: genreFilter,
});
await loadPageEfficiently(newPage.id);
hideLoadingIndicator();
}
Error Recovery & Stale Data
If a row fetch fails (network error), show an error state with a retry button. Don’t pre-fetch rows that might go stale—lazy loading prevents this by fetching only when needed.
async function loadRowItemsWithErrorHandling(pageId, row) {
try {
const items = await discovr.getRowItems(pageId, row.id);
renderRow(row, items);
} catch (err) {
renderRowError(row);
row.retryCallback = () =>
loadRowItemsWithErrorHandling(pageId, row);
}
}
Metrics to Measure
Track these to ensure your lazy loading is working:
- Time to Hero: When does the hero appear? (Target: under 1s)
- Time to First Row: When does the first row become visible? (Target: under 2s)
- Total Page Load: When is the page fully interactive? (Target: under 3s)
- Bandwidth per Page: Compare lazy vs. eager loading. Lazy should use 40-60% less bandwidth.
- Scroll Smoothness: Measure frame rate. Lazy loading should prevent jank (60 FPS on modern devices).
Use your browser’s Network tab or mobile profiler to measure. For iOS, use Xcode’s System Trace. For Android, use Android Profiler. Look for network waterfalls and ensure hero fetches complete quickly.
Next: Error handling