Frontend Development 13 min read

How to Fix Concurrent Rendering Issues in OpenSumi File Tree for Faster, Stable UI

This article analyzes the concurrent rendering problems of OpenSumi's file‑tree component, explains their root causes, and presents a comprehensive solution—including operation prioritization, cancellable updates, and queued rendering—along with implementation code to achieve both high speed and stability.

Taobao Frontend Technology
Taobao Frontend Technology
Taobao Frontend Technology
How to Fix Concurrent Rendering Issues in OpenSumi File Tree for Faster, Stable UI

Introduction

In the OpenSumi framework, all Tree components use a flat rendering structure. In file‑tree scenarios, external file changes, tree operations, and editor actions can trigger many refresh requests, leading to concurrent rendering issues that cause abnormal rendering, especially after removing throttling in version 2.16.0.

While the removal improved response speed (under 100 ms), stability suffered. This article focuses on stabilizing the file tree while maintaining fast responses.

Problems

Duplicate Nodes

Repeated rendering nodes appear, especially in compressed paths like

a/b/c/d

, as shown in the screenshot.

Refresh‑Operation Conflict

Frequent refreshes cause the tree state to jump back and forth, creating a poor user experience. A test case creates a file every 200 ms, triggering refreshes that overlap with user actions, leading to rendering errors on slower machines and a “sticky” feel due to throttling.

Root Cause Analysis

The file tree relies on OpenSumi's RecycleTree component, which separates data from view and uses a flat data structure for performance. Two main issues cause anomalies:

Abnormal truncation or extension of the rendering node list stored in

TreeModel

.

Cached nodes in

TreeNode

are not updated correctly.

During rendering, data updates in

TreeModel

cannot be cancelled, and interleaved view updates corrupt the final output. Additionally, operations have priority: expand/collapse should outrank node locating, which outranks refresh.

Solution

1. Separate Core and Non‑Core Operations

Core operations (expand, collapse) must execute immediately, while non‑core operations (snapshot restore, locate, refresh) can be cancelled when a core operation occurs.

Conflict Handling

Expanding a node during a refresh.

Locating a node during a refresh.

Locating and then expanding during a refresh.

Locating, expanding, and immediately collapsing during a refresh.

Illustrative diagrams are provided in the original images.

2. Enable Cancellation Before Rendering

Refresh operations must be interruptible. The new approach stores pending updates in a

Children

property, merges them only after all children are loaded, and allows cancellation via a

CancellationToken

.

3. Queue Data Updates and Rendering

Refreshes are queued so that a new refresh waits for the previous one to finish, reducing redundant work and improving performance.

Implementation

The following pseudocode shows the revised

TreeNode.refresh

method with cancellation support:

<code>class TreeNode { ...
  refresh(
    expandedPaths: string[] = this.getAllExpandedNodePath(),
    token?: CancellationToken,
  ) {
    const childrens = (await this._tree.resolveChildren(this)) || [];
    if (token?.isCancellationRequested) { return; }
    while ((forceLoadPath = expandedPaths.shift())) {
      const child = childrens?.find(child => child.path === forceLoadPath);
      if (CompositeTreeNode.is(child)) {
        await child.resolveChildrens();
        if (token?.isCancellationRequested) { return; }
        await child.refresh(expandedPaths, token);
        if (token?.isCancellationRequested) { return; }
      }
    }
    if (forceLoadPath) {
      expandedPaths.unshift(forceLoadPath);
      this.expandBranch(this, true);
    } else if (CompositeTreeNode.isRoot(this)) {
      const expandedChilds: CompositeTreeNode[] = [];
      const flatTree = new Array(childrens.length);
      this._children = new Array(childrens.length);
      for (let i = 0; i < childrens.length; i++) {
        const child = childrens[i];
        this._children[i] = child;
        if (CompositeTreeNode.is(child) && child.expanded) { expandedChilds.push(child); }
      }
      this._children.sort(this._tree.sortComparator || CompositeTreeNode.defaultSortComparator);
      for (let i = 0; i < childrens.length; i++) { flatTree[i] = this._children[i].id; }
      this._branchSize = flatTree.length;
      this.setFlattenedBranch(flatTree, true);
      for (let i = 0; i < expandedChilds.length; i++) {
        const child = expandedChilds[i];
        child.expandBranch(child, true);
      }
    }
  }
  ...
}
</code>

Global tree state is managed via a static map, allowing any node to cancel ongoing locate or refresh tasks before starting a new core operation:

<code>class TreeNode { ...
  public static pathToGlobalTreeState: Map<string, IGlobalTreeState> = new Map();
  public static getGlobalTreeState(path: string) {
    const root = path.split(Path.separator).slice(0, 2).join(Path.separator);
    let state = TreeNode.pathToGlobalTreeState.get(root);
    if (!state) {
      state = {
        isExpanding: false,
        isLoadingPath: false,
        isRefreshing: false,
        refreshCancelToken: new CancellationTokenSource(),
        loadPathCancelToken: new CancellationTokenSource(),
      };
    }
    return state;
  }
  public static setGlobalTreeState(path: string, updateState: IOptionalGlobalTreeState) {
    const root = path.split(Path.separator).slice(0, 2).join(Path.separator);
    let state = TreeNode.pathToGlobalTreeState.get(root);
    if (!state) {
      state = {
        isExpanding: false,
        isLoadingPath: false,
        isRefreshing: false,
        refreshCancelToken: new CancellationTokenSource(),
        loadPathCancelToken: new CancellationTokenSource(),
      };
    }
    state = { ...state, ...updateState };
    TreeNode.pathToGlobalTreeState.set(root, state);
    return state;
  }
  ...
  public async setExpanded(ensureVisible = true, quiet = false, isOwner = true, token?: CancellationToken) {
    const state = TreeNode.getGlobalTreeState(this.path);
    state.loadPathCancelToken.cancel();
    state.refreshCancelToken.cancel();
    TreeNode.setGlobalTreeState(this.path, { isExpanding: true });
    this.isExpanded = true;
    if (this._children === null) {
      await this.hardReloadChildren(token);
    }
    ...
  }
  ...
}
</code>

All conflict handling follows the same pattern: cancel existing tasks, set the appropriate global state, and proceed with the core operation.

Final Effect

Under frequent file changes, the revised logic ensures smooth user interactions while keeping the file‑tree state up‑to‑date.

Conclusion

The file‑tree may appear simple, but achieving both high performance and excellent interaction experience requires careful handling of concurrent operations, cancellation, and queuing. The presented approach, combined with community collaboration, brings the file‑tree to a “complete” stability and performance stage.

FrontendPerformance OptimizationConcurrent RenderingOpenSumitree componentcancellation token
Taobao Frontend Technology
Written by

Taobao Frontend Technology

The frontend landscape is constantly evolving, with rapid innovations across familiar languages. Like us, your understanding of the frontend is continually refreshed. Join us on Taobao, a vibrant, all‑encompassing platform, to uncover limitless potential.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.