How JD Daojia Achieved 60 FPS List Scrolling with Asynchronous UI Rendering
This article explains how JD Daojia tackled heavy visual elements in its shopping app by redesigning UI components as asynchronous render nodes, reducing main‑thread work and GPU load, resulting in smoother scrolling up to 60 FPS even on older iPhones.
Background
In the JD Daojia app the product list screens became densely packed with visual effects such as shadows, rounded corners and gradients. The growing number of UI components and deep view hierarchies caused noticeable sluggishness when scrolling large lists.
Analysis
iOS renders at 60 Hz using a double‑buffer and VSync mechanism. Each frame must be prepared on the CPU (layout, view creation, image decoding, text layout) and then handed to the GPU for compositing. If the CPU or GPU cannot finish a frame within 1/60 second, the frame is dropped, resulting in lower FPS and perceived lag. The main contributors to frame drops are:
Excessive work on the main thread.
Complex layout calculations.
Off‑screen rendering caused by masks, shadows, or non‑rectangular corners.
Deep view hierarchies that increase compositing cost.
Solution
The UI was re‑architected around lightweight Node objects that map to UIView but can be created and manipulated off the main thread. A small set of atomic nodes— BaseNode, ImageNode, LabelNode —serve as building blocks; custom components inherit from BaseNode and implement their own drawing logic.
// DJAsyncBaseNode.h
#import <Foundation/Foundation.h>
#import "DJAsyncAssistant.h"
@class DJAsyncContainer;
@interface DJAsyncBaseNode : NSObject
@property(nonatomic,assign) CGRect frame;
@property(nonatomic,assign) NSInteger tag;
@property(atomic,assign) BOOL hidden;
@property(atomic,assign) BOOL clipsToBounds;
@property(nullable, atomic, copy) UIColor *backgroundColor;
@property (atomic, nullable, copy) UIColor *backgroundStartColor;
@property (atomic, nullable, copy) UIColor *backgroundEndColor;
@property (atomic, nullable, copy) UIColor *borderColor;
@property (atomic, assign) CGFloat borderWidth;
@property (nonatomic, assign) CGFloat cornerRadius;
@property (nonatomic, assign) DJAsyncBaseNodeRadius radius;
@property (atomic, readonly, strong) UIBezierPath * _Nullable clipPath;
@property(nullable, nonatomic,readonly,weak) DJAsyncBaseNode *superNode;
@property(nonatomic,readonly,strong) NSMutableArray<__kindof DJAsyncBaseNode *> *subNodes;
@property(nonatomic,weak) DJAsyncContainer * _Nullable containerRef;
- (void)removeFromSuperNode;
- (void)addSubNode:(DJAsyncBaseNode *)node;
- (void)asyncDrawRect:(CGRect)rect inContext:(CGContextRef _Nonnull)context;
- (CGRect)absoluteRect;
@endWhen a layer needs to be displayed, the container view’s -displayLayer: method forwards the node tree to an AsyncDrawEngine. The engine:
Maintains a thread‑pool for drawing tasks.
Distributes each node’s asyncDrawRect:inContext: call to a background thread.
Renders the node hierarchy off‑screen into a UIImage.
Caches the bitmap for reuse.
Sets the CALayer’s contents to the cached image on the main thread.
Interactive elements (e.g., add/remove buttons) remain as live UI components. After the asynchronous draw finishes, the callback -asyncDrawDidFinished allows developers to decide which nodes stay interactive and which are merged into the static bitmap.
Implementation Details
Node definition : Extend BaseNode to add custom properties (gradient colors, irregular corner radii, clipping paths) and override asyncDrawRect:inContext: for bespoke drawing.
Container view : Subclass UIView and implement -displayLayer: to invoke the engine.
AsyncDrawEngine : Use dispatch_queue or NSOperationQueue to create a fixed‑size thread pool; each draw task records the node’s absolute rect, draws into a CGBitmapContext, creates a UIImage, and stores it in an NSCache keyed by node hierarchy hash.
Caching strategy : Cache is invalidated when a node’s visual properties change (e.g., background color, image source) or when the view size changes.
Interaction handling : Nodes that require user interaction are excluded from the bitmap and kept as separate sublayers; the engine notifies the container via -asyncDrawDidFinished so the container can re‑attach those interactive sublayers.
Practice / Results
In the product list the original view hierarchy contained dozens of layers for promotions, badges, masks and custom corners. After applying the async‑node approach the hierarchy collapsed to a single background layer plus a few interactive button layers. Performance measurements showed:
Major reduction of main‑thread CPU spikes during fast scrolling.
Lower GPU utilization because most drawing is performed off‑screen.
Stable 60 FPS on high‑end iPhones; on an iPhone 5s the frame‑rate increased from frequent drops to a mostly smooth experience.
Visual comparisons (included below) illustrate the simplified layer tree and the before/after frame‑rate graphs.
Conclusion
Moving from a traditional, fully‑materialized view hierarchy to an asynchronous node‑based rendering pipeline dramatically improves scrolling smoothness and reduces both CPU and GPU load. The approach leverages multi‑core CPUs by off‑loading layout and drawing work to background threads while keeping only truly interactive elements on the main thread. Simpler UI designs further amplify these gains, as fewer visual effects translate to lower rendering cost.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Dada Group Technology
Sharing insights and experiences from Dada Group's R&D department on product refinement and technology advancement, connecting with fellow geeks to exchange ideas and grow together.
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.
