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.
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
TreeNodeare not updated correctly.
During rendering, data updates in
TreeModelcannot 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
Childrenproperty, 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.refreshmethod 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.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.