Skip to content

[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

Open

Description

@SylvainLegrand

⚠️ 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 one IView per folder (generateNodeView(), L119–L139) and calls Navigation.register(...views) (L110) — 4,140 views for one folder, every one of them a reactive entry of Navigation.views.
    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.vue renders one NcAppNavigationItem per view and recurses (v-for="childView in sortedChildViews"), and for each item computes childViews by 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).
    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

  1. Virtualize the tree panel (render only the visible rows, e.g. with vue-virtual-scroller which 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).
  2. Lazy-load one level on expand only (depth=1), do not pre-register grandchildren, and do not turn every folder into a navigation IView: keep one view for the tree and let the component own its node tree.
  3. Index children by parent once (a Map<parentId, IView[]>) instead of filtering all views per item.
  4. 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

  1. Create a folder with 4,000+ subfolders (empty subfolders are enough).
  2. Keep the default "Enable folder tree" setting (Files → settings).
  3. Open that folder in the Files app with the browser's performance tools recording (Long Tasks / Performance tab).
  4. Count the items in the left navigation panel (document.querySelectorAll('[data-cy-files-navigation-item]').length).
  5. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    0. Needs triagePending check for reproducibility or if it fits our roadmap34-feedbackbug

    Type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    ⌂ Proxy