Performance Considerations When Using SectionKit: 7 Optimization Strategies

Avoid heavy object instantiation in config(_:), enable size caching via SKHighPerformanceStore, and use incremental updates instead of full reloads to maintain 60fps scrolling in SectionKit.

SectionKit is designed for smooth scrolling and low-overhead updates, but achieving optimal performance requires understanding its architectural hotspots. When building complex collection views with the linhay/sectionkit repository, developers must account for how the framework handles cell configuration, size calculation, and memory management. This guide covers the critical performance considerations when using SectionKit, from anti-patterns to avoid in AGENTS.md to the high-performance caching APIs implemented in Sources/SectionKit/HighPerformance/.

Avoid Heavy Work Inside config(_:)

The framework creates cells and sections repeatedly during reloads. Instantiating expensive objects—especially DateFormatter, NumberFormatter, or complex view layouts—inside config(_:) executes on the main thread for every model and can dominate frame time.

The anti-pattern list in AGENTS.md explicitly warns:

“Never instantiate formatters in config(_:). Use static.”

Reuse Expensive Objects

Create formatters once and reuse them across all cells:

// ❌ Bad – creates a new formatter for every cell
func config(_ model: Model) {
    let df = DateFormatter()
    df.dateFormat = "yyyy-MM-dd"
    label.text = df.string(from: model.date)
}

// ✅ Good – static, reused across all cells
private static let sharedFormatter: DateFormatter = {
    let f = DateFormatter()
    f.dateFormat = "yyyy-MM-dd"
    return f
}()

func config(_ model: Model) {
    label.text = Self.sharedFormatter.string(from: model.date)
}

Enable High-Performance Size Caching

When cells use Auto Layout or complex view hierarchies, recomputing sizes for every item becomes a major bottleneck. SectionKit provides a size-caching store that memorizes results for a model-identifier and limit-size pair.

Core Caching Components

The implementation relies on two key files:

Implementing Size Caching

Enable caching by calling setHighPerformance(.init()) and providing a unique identifier for each model:

// 1️⃣ Define a Hashable model
struct Photo: Hashable {
    let id: UUID
    let imageURL: URL
    let title: String
}

// 2️⃣ Build the section with high-performance mode
let photoSection = PhotoCell.wrapperToSingleTypeSection()
    .setHighPerformance(.init())                 // turn on caching
    .highPerformanceID { context in
        context.model.id                         // unique per-item identifier
    }
    .config(models: photos)

// 3️⃣ Add to manager
manager.update([photoSection])

The framework queries SKHighPerformanceStore for a cached size before falling back to the expensive layout pass.

Profile Execution Time with SKPerformance

Debug builds can measure execution time using SKPerformance.duration to log millisecond counts and maintain running averages. This helps identify hot paths like model-to-cell mapping or image decoding.

Located in Sources/SectionKit/Common/SKPerformance.swift, the utility provides zero runtime cost in production builds when DEBUG is undefined.

let section = SKPerformance.shared.duration("Section setup") {
    ItemCell.wrapperToSingleTypeSection()
        .config(models: items)
}
manager.update([section])

Debug output appears as:


[SKPerformance] /Sources/Example/.../MyViewController.swift:123: Section setup: 耗时 0.0023 秒

Prevent Memory Retain Cycles in Closures

Section-level closures like onCellAction or model(_:displayedAt:) retain captured values. Strong references to the surrounding view controller create retain cycles, forcing the section hierarchy to stay alive and increasing memory pressure.

The AGENTS.md anti-patterns section explicitly warns:

“Never capture self strongly in section closures.”

Always use [weak self]:

section.onCellAction(.selected) { [weak self] ctx in
    guard let self = self else { return }
    self.showDetail(for: ctx.model)
}

Optimize Layout and Plugin Interactions

Plugins modifying layout (pinning, decorations, alignment) remain cheap when they only affect layout attributes. However, mixing them with native UICollectionViewFlowLayout pinning options causes the layout engine to recompute attributes twice per frame, degrading scroll performance.

The anti-pattern list in AGENTS.md specifically prohibits:

“Never mix SKCSectionPinOptions with FlowLayout's own pinning.”

Prefer Batch Updates Over Full Reloads

SKCManager in Sources/SectionKit/Manager/SKCManager.swift supports incremental changes (insert, delete, reload) instead of calling reloadData. Incremental updates trigger only affected sections or rows to be re-measured and animated, reducing per-frame work.

manager.update(sections)               // diff-based incremental update
// vs.
manager.reload(section)                // full reload of the section

Summary

  • Reuse expensive objects: Never instantiate DateFormatter or heavy views inside config(_:); use static properties instead.
  • Enable size caching: Use setHighPerformance(.init()) and highPerformanceID to leverage SKHighPerformanceStore and avoid redundant Auto Layout passes.
  • Profile hot paths: Wrap suspicious code in SKPerformance.shared.duration() to identify bottlenecks during development.
  • Prevent retain cycles: Always capture [weak self] in section closures to avoid memory leaks and unnecessary layout passes.
  • Avoid layout conflicts: Do not mix SKCSectionPinOptions with UICollectionViewFlowLayout pinning to prevent double attribute calculations.
  • Prefer incremental updates: Use manager.update(sections) for diff-based changes rather than full reloads to minimize re-measurement work.

Frequently Asked Questions

How does SectionKit's size caching improve scrolling performance?

SectionKit's size caching stores calculated cell dimensions in SKHighPerformanceStore, backed by the NSCache wrapper SKKVCache. When scrolling, the framework checks for a cached size using the model's unique identifier before executing expensive Auto Layout calculations. This eliminates redundant measurement work during rapid scroll events, maintaining 60fps performance even with complex cell hierarchies.

What is the performance cost of using SKPerformance in production builds?

SKPerformance has zero runtime cost in production builds. The duration method and related APIs are compiled out when DEBUG is undefined, meaning the closure executes directly without timing overhead. This allows developers to instrument critical paths during development without worrying about shipping performance-monitoring code to users.

Why should I avoid instantiating DateFormatter inside config(_:)?

config(_:) executes on the main thread for every cell during reloads and scrolls. DateFormatter initialization is notoriously expensive due to internal locale and calendar setup. Creating a new instance for each cell causes frame drops during rapid scrolling. The recommended approach uses a static formatter property created once and reused across all cells, as documented in the AGENTS.md anti-patterns guide.

When should I use batch updates instead of reloadData?

Use batch updates via manager.update(sections) whenever the data set changes incrementally (insertions, deletions, moves, or updates to specific items). This triggers UICollectionView's batch update animations and only re-measures affected cells. Reserve reloadData or manager.reload(section) for complete data set replacements or when the data source structure changes fundamentally, as full reloads discard all existing cells and recompute the entire layout.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →