From c958d49db93fb6aace325e5b4c9a6db7bb17ffa4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 8 Nov 2025 22:30:30 +0000 Subject: [PATCH 1/2] Initial plan From db0acf3715beb4b13d82b2c3f72510057849dd6c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 8 Nov 2025 22:37:47 +0000 Subject: [PATCH 2/2] Implement list virtualization for Home feed with @tanstack/react-virtual Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com> --- package.json | 1 + pnpm-lock.yaml | 41 +++++++++++++++++++++++++ src/components/Feed.tsx | 66 +++++++++++++++++++++++++++++++++-------- src/lib/manager.ts | 8 ++--- 4 files changed, 98 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 1c4e31f..51cc61f 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ }, "dependencies": { "@fontsource/jersey-10": "^5.2.6", + "@tanstack/react-virtual": "^3.13.12", "dexie": "^4.0.11", "html-to-image": "^1.11.13", "lodash": "^4.17.21", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 745b3a1..ba84ff3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@fontsource/jersey-10': specifier: ^5.2.6 version: 5.2.6 + '@tanstack/react-virtual': + specifier: ^3.13.12 + version: 3.13.12(react-dom@19.2.0(react@19.2.0))(react@19.2.0) dexie: specifier: ^4.0.11 version: 4.0.11 @@ -987,6 +990,15 @@ packages: peerDependencies: '@svgr/core': '*' + '@tanstack/react-virtual@3.13.12': + resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/virtual-core@3.13.12': + resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} + '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} @@ -1750,6 +1762,15 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} + react-dom@19.2.0: + resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} + peerDependencies: + react: ^19.2.0 + + react@19.2.0: + resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} + engines: {node: '>=0.10.0'} + readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -1807,6 +1828,9 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + scroll-lock@2.1.5: resolution: {integrity: sha512-GN8Lp0AzXbkrPFUUNkMUruiiv019UvarNKE/SnXi+AxZRjMnDc2R22VB9RcUtL4P/uub04cKibmpHKIKTyWwYQ==} deprecated: 'Please use the new version: https://github.com/fluejs/noscroll' @@ -3142,6 +3166,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@tanstack/react-virtual@3.13.12(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@tanstack/virtual-core': 3.13.12 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + '@tanstack/virtual-core@3.13.12': {} + '@types/estree@1.0.7': {} '@types/http-proxy@1.17.16': @@ -3922,6 +3954,13 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + react-dom@19.2.0(react@19.2.0): + dependencies: + react: 19.2.0 + scheduler: 0.27.0 + + react@19.2.0: {} + readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -4003,6 +4042,8 @@ snapshots: safer-buffer@2.1.2: {} + scheduler@0.27.0: {} + scroll-lock@2.1.5: {} semver@6.3.1: {} diff --git a/src/components/Feed.tsx b/src/components/Feed.tsx index a65915e..0506781 100644 --- a/src/components/Feed.tsx +++ b/src/components/Feed.tsx @@ -1,4 +1,5 @@ -import { useMemo, useEffect } from "react"; +import { useMemo, useEffect, useRef } from "react"; +import { useVirtualizer } from "@tanstack/react-virtual"; import { _ } from "~/lib/i18n"; import { GRAY_COLOR } from "~/constants"; @@ -10,12 +11,14 @@ interface Props { } export default function Feed({ posts }: Props) { - const items = posts.map((p) => - useMemo( - () => , - [p.id, p.likes, p.liked, p.replies], - ), - ); + const parentRef = useRef(null); + + const virtualizer = useVirtualizer({ + count: posts.length, + getScrollElement: () => window.document.documentElement, + estimateSize: () => 200, + overscan: 5, + }); useEffect(() => { const pos = sessionStorage.feedScrollPos; @@ -25,11 +28,11 @@ export default function Feed({ posts }: Props) { } }, []); - return ( -
- {posts.length ? ( - items - ) : ( + const items = virtualizer.getVirtualItems(); + + if (!posts.length) { + return ( +

{_("No posts yet")}

- )} +
+ ); + } + + return ( +
+
+ {items.map((virtualItem) => { + const post = posts[virtualItem.index]; + return ( +
+ {useMemo( + () => ( + + ), + [post.id, post.likes, post.liked, post.replies], + )} +
+ ); + })} +
); } diff --git a/src/lib/manager.ts b/src/lib/manager.ts index 423f7d0..28c2ef1 100644 --- a/src/lib/manager.ts +++ b/src/lib/manager.ts @@ -42,7 +42,7 @@ export class Manager { await db.posts.where("active").below(oldDate).delete(); const onPostsChanged = throttle(async () => { - setPosts(await db.posts.orderBy("active").reverse().limit(500).toArray()); + setPosts(await db.posts.orderBy("active").reverse().toArray()); }, 500); this.queue.push({ payload: { @@ -120,13 +120,11 @@ export class Manager { } async getReplies(postId: string): Promise { - return (await db.replies.where({ postId }).reverse().sortBy("date")).slice( - -500, - ); + return await db.replies.where({ postId }).reverse().sortBy("date"); } async getAllReplies(): Promise { - return await db.replies.orderBy("date").reverse().limit(500).toArray(); + return await db.replies.orderBy("date").reverse().toArray(); } private async processUpdate(update: ReceivedStatusUpdate) {