Repository navigation
[Bug]: Files "Folder tree" navigation registers and renders one navigation view per folder (no virtualization): 12,490 DOM items, 7.1 s page load and a 2.3 s long task with 4,140 subfolders #65905
Description
⚠️ This issue respects the following points: ⚠️
- This is not a troubleshooting question, general support matter, or webserver/proxy problem, but likely a bug (if unsure, ask the Community Help Forum).
- This issue is not already reported on Github OR solved at the Community Help Forum (I've searched!).
- I'm using a maintained major version of Nextcloud Server and tested against the latest patch level. (Supported major versions and current patch levels).
- I agree to follow Nextcloud's Code of Conduct.
- I've tried my best to provide clear reproduction steps that someone unfamiliar with this bug could use to reproduce it.
Bug description
The "Folder tree" navigation view (Nextcloud 30+, apps/files/src/views/folderTree.ts) registers one navigation view per folder (Navigation.register(...views)) and the navigation panel renders one NcAppNavigationItem per view, recursively, without any virtualization. With a folder that contains 4,140 subfolders, opening it renders 12,490 items in the navigation panel, the page load goes from 0.82 s to 7.1 s, and the longest blocking task on open goes from 75 ms to 2.3 s (main thread frozen). The whole app stays sluggish afterwards because every reactive change re-evaluates childViews for every item.
Limiting the server response (see the companion server-side issue about ApiController::getChildren(), #65904) is not enough: the panel also registers the folders already loaded by the files list (updateTreeChildren() reuses the files store), so the cost is paid as soon as the folder is opened.
Actual behavior
| Measurement (Chrome, same account, same folder with 4,140 subfolders) | Folder tree enabled | Folder tree disabled |
|---|---|---|
| Items rendered in the navigation panel | 12,490 | 32 |
| Page load of the folder with 4,140 subfolders | 7.1 s | 0.82 s |
Page load of a project folder 3 levels below it (/<clients>/<client>/<project>/<subfolder>) |
3.1 s | 0.63 s |
| Longest blocking task on open | 2.3 s | 75 ms |
| Jump of the files list to a given entry | 0.4–0.8 s | 0.2–0.28 s |
Server side, the folder-tree request itself is fast (~0.2 s with the fix proposed in #65904); the time is spent in the browser.
Root cause
apps/files/src/views/folderTree.ts (identical on stable34 and master):
registerNodeViews()(L87–L110) builds oneIViewper folder (generateNodeView(), L119–L139) and callsNavigation.register(...views)(L110) — 4,140 views for one folder, every one of them a reactive entry ofNavigation.views.
server/apps/files/src/views/folderTree.ts
Lines 87 to 139 in 3c5546a
async function registerNodeViews(nodes: (TreeNode | IFolder)[], path?: string) { const views: IView[] = [] for (const node of nodes) { const isRegistered = Navigation.views.some((view) => view.id === `${folderTreeId}::${node.encodedSource}`) // skip hidden files if the setting is disabled if (!showHiddenFiles && node.basename.startsWith('.')) { if (isRegistered) { // and also remove any existing views for hidden files if the setting was toggled Navigation.remove(`${folderTreeId}::${node.encodedSource}`) } continue } // skip already registered views to avoid duplicates when loading multiple levels if (isRegistered) { continue } views.push(generateNodeView( node, path === node.path || path?.startsWith(node.path + '/') ? true : undefined, )) } Navigation.register(...views) } /** * Generates a navigation view for a given folder tree node or folder. * * @param node - The folder tree node or folder for which to generate the view. * @param expanded - Whether the view should be expanded by default. */ function generateNodeView(node: TreeNode | IFolder, expanded?: boolean): IView { return { id: `${folderTreeId}::${node.encodedSource}`, parent: getSourceParent(node.source), expanded, loaded: expanded, // @ts-expect-error Casing differences name: node.displayName ?? node.displayname ?? node.basename, icon: FolderSvg, getContents, loadChildViews: getLoadChildViews(node), params: { view: folderTreeId, fileid: String(node.fileid), // Needed for matching exact routes dir: node.path, }, updateTreeChildren()(L171–L183) registers the folders already present in the files store (L173–L178), otherwise fetches two levels at once (getFolderTreeNodes(path, 2), L181), so expanding a node loads and registers the grandchildren too.apps/files/src/components/FilesNavigationListItem.vuerenders oneNcAppNavigationItemper view and recurses (v-for="childView in sortedChildViews"), and for each item computeschildViewsby filtering all visible views (views.value.filter((v) => v.parent === props.view.id), L32–L36): O(views²) on every change (12,490² ≈ 155 M comparisons).
server/apps/files/src/components/FilesNavigationListItem.vue
Lines 30 to 66 in 07b7a0f
const views = useVisibleViews() const childViews = computed(() => { if (props.level < maxLevel) { return views.value.filter((v) => v.parent === props.view.id) } else { return views.value.filter((v) => isDescendant(v, props.view.id)) } /** * Check if a view is a descendant of another view by recursively traversing up the parent chain. * * @param view - The view to check * @param parent - The parent view id to check against */ function isDescendant(view: IView, parent: string): boolean { if (!view.parent) { return false } else if (view.parent === parent) { return true } const parentView = views.value.find((v) => v.id === view.parent) return !!parentView && isDescendant(parentView, parent) } }) const sortedChildViews = computed(() => childViews.value.slice().sort((a, b) => { if (a.order !== undefined && b.order === undefined) { return -1 } else if (a.order === undefined && b.order !== undefined) { return 1 } return collator.compare(a.name, b.name) })) const hasChildViews = computed(() => childViews.value.length > 0) const activeViewId = inject('currentNavigationView', ref<string | undefined>(undefined)) const isActiveView = computed(() => activeViewId.value.id === props.view.id)
Proposed fix
- Virtualize the tree panel (render only the visible rows, e.g. with
vue-virtual-scrollerwhich the Files app already ships, on a flattened array of{ node, level, expanded }), or at least paginate children per node ("show more" beyond N entries). - Lazy-load one level on expand only (
depth=1), do not pre-register grandchildren, and do not turn every folder into a navigationIView: keep one view for the tree and let the component own its node tree. - Index children by parent once (a
Map<parentId, IView[]>) instead of filtering all views per item. - Sort with a cached
Intl.Collator(numeric, base sensitivity).
Feasibility: a quick prototype injected into the same page (virtualized list in place of the panel content, same look) on the same 4,140 subfolders renders 24 items instead of 12,490, first paint in 85 ms, 67 ms per scroll jump, and sorts the 4,140 names in 1.3 ms with Intl.Collator, with the same order as the files list.
Steps to reproduce
- Create a folder with 4,000+ subfolders (empty subfolders are enough).
- Keep the default "Enable folder tree" setting (Files → settings).
- Open that folder in the Files app with the browser's performance tools recording (Long Tasks / Performance tab).
- Count the items in the left navigation panel (
document.querySelectorAll('[data-cy-files-navigation-item]').length). - Compare with the same folder after disabling the folder tree (
occ user:setting <uid> files folder_tree 0, reload).
Expected behavior
The navigation panel should render only the visible rows (virtualized or paginated list), load children lazily on expand only, and never block the main thread for seconds. Opening a folder should cost about the same with or without the folder tree.
Nextcloud Server version
34
Operating system
Debian/Ubuntu
PHP engine version
PHP 8.4
Web server
Other
Database engine version
PostgreSQL
Is this bug present after an update or on a fresh install?
Fresh Nextcloud Server install
Are you using the Nextcloud Server Encryption module?
Encryption is Disabled
What user-backends are you using?
- Default user-backend (database)
- LDAP/ Active Directory
- SSO - SAML
- Other
Configuration report
List of activated Apps
Nextcloud Signing status
Nextcloud Logs
Additional info
- Nextcloud Server 34.0.3 —
folderTree.tsunchanged onmasteras of 2026-10-24 (3c5546a1a84230af62c981e9512d977656ee459c) and onstable34(07b7a0f600708c90ba94ede238818c215536d4bf). The folder tree view was introduced by feat: Navigate via folder tree #46596 / feat(files): Load limited depth tree #47122 (Nextcloud 30); perf(files): initialize folder tree from current state and store #58273 (Nextcloud 34) reuses the files store but does not change the one-view-per-folder design. - Server: Ubuntu 24.04 LTS, PHP 8.4, PostgreSQL 16, Caddy 2 (FastCGI to php-fpm — hence "Other" above). Client: Chrome, current stable.
- The only workaround today is to disable the folder tree per account (
occ user:setting <uid> files folder_tree 0), which removes the feature; the default is hard-coded totrue(apps/files/lib/Service/UserConfig.php), so new accounts get it back. - Companion server-side issue: [Bug]: Files folder-tree API eagerly lists the children of every subfolder at the last requested depth, then discards them (N extra directory listings per request) #65904 (the API computes and discards the listings of all subfolders).
- Measurement protocol:
PerformanceObserveronlongtaskentries + time to first rendered rows; happy to share the script and to test a fix on the real tree.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsTo triage
Activity