โ† Back to Blog

March 14, 2026 ยท 14 min read

SDUI Performance Optimization: Making Server-Driven UI Fast

"Isn't SDUI slower than native?" It doesn't have to be. Learn the techniques that make production SDUI systems feel instant.

1. Setting Your Performance Budget

Before optimizing, define what "fast" means for your app. Here's a framework:

Metric Good Acceptable Poor
Time to First Paint < 100ms < 300ms > 500ms
Time to Interactive < 200ms < 500ms > 1000ms
Layout JSON size < 20KB < 50KB > 100KB
Network requests 1 2-3 > 5
Scroll FPS 60fps 45-60fps < 30fps

๐Ÿ’ก The Goal: Indistinguishable from Native

Users shouldn't notice the difference between SDUI screens and bundled screens. If they can, you have a performance problem.

2. Network Optimization

Network latency is often the biggest performance bottleneck. Here's how to minimize it.

Gzip/Brotli Compression

JSON compresses extremely well. Enable compression on your server:

// Express.js example
const compression = require('compression');
app.use(compression({
    level: 6, // Balance between speed and compression
    threshold: 1024, // Only compress responses > 1KB
    filter: (req, res) => {
        return req.headers['accept-encoding']?.includes('gzip');
    }
}));

JSON Compression Ratio (Home Screen Layout)

Uncompressed
48 KB
Gzip
8.6 KB
Brotli
6.7 KB

CDN Edge Caching

Serve layouts from the edge for sub-50ms response times:

// Cache-Control header for CDN
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=86400

// Breakdown:
// - public: Can be cached by CDN
// - max-age=60: Client caches for 60s
// - s-maxage=300: CDN caches for 5 min
// - stale-while-revalidate: Serve stale while fetching fresh

JSON Minification

Remove unnecessary whitespace and shorten keys:

// Before: 847 bytes
{
    "componentType": "text",
    "properties": {
        "textContent": "Hello World",
        "textStyle": {
            "fontSize": 16
        }
    }
}

// After: 51 bytes
{"t":"tx","p":{"c":"Hello World","s":{"f":16}}}

HTTP/2 & HTTP/3

Use modern protocols for multiplexing and header compression:

3. Caching Strategies

A well-designed cache can make SDUI faster than bundled UI because you can update it.

Multi-Layer Cache

class SDUICache {
    // L1: In-memory (instant)
    private val memory = LruCache<String, Layout>(50)
    
    // L2: Disk (survives restart)
    private val disk = DiskCache(context, 50.MB)
    
    // L3: CDN (network)
    private val cdn = CdnClient(baseUrl)
    
    suspend fun get(route: String): Layout {
        // Check L1 โ†’ L2 โ†’ L3
        memory.get(route)?.let { return it }
        
        disk.get(route)?.let { 
            memory.put(route, it)
            return it 
        }
        
        val fresh = cdn.fetch(route)
        disk.put(route, fresh)
        memory.put(route, fresh)
        return fresh
    }
}

Stale-While-Revalidate

Show cached content immediately, fetch fresh in background:

suspend fun loadScreen(route: String): Flow<Layout> = flow {
    // Emit cached immediately (instant load)
    cache.get(route)?.let { 
        emit(it)
    }
    
    // Fetch fresh in background
    val fresh = api.fetchLayout(route)
    
    // Only emit if different from cached
    if (fresh.hash != cache.get(route)?.hash) {
        cache.put(route, fresh)
        emit(fresh)
    }
}

Differential Updates

Only fetch what changed, not the entire layout:

// Request
GET /api/layout/home
If-None-Match: "abc123"

// Response (304 if unchanged)
304 Not Modified

// Or with JSON Patch for partial updates
200 OK
Content-Type: application/json-patch+json

[
    { "op": "replace", "path": "/banner/title", "value": "New Title" }
]

4. Render Performance

Once you have the JSON, rendering needs to be fast.

Component Recycling

Reuse component instances like RecyclerView/LazyColumn:

// Android: Use LazyColumn for lists
@Composable
fun SDUIList(items: List<JsonNode>) {
    LazyColumn {
        items(
            items = items,
            key = { it.id }, // Stable keys for recycling
            contentType = { it.type } // Same type = reuse
        ) { node ->
            SDUIComponent(node)
        }
    }
}

Memoization

Skip re-renders when props haven't changed:

// Remember parsed components
@Composable
fun SDUIComponent(node: JsonNode) {
    val component = remember(node.hash) {
        parseComponent(node)
    }
    component.Render()
}

// Use derivedStateOf for expensive derivations
val visibleItems by remember {
    derivedStateOf {
        items.filter { it.isVisible }
    }
}

Async Component Parsing

Parse JSON off the main thread:

suspend fun parseLayout(json: String): Layout = 
    withContext(Dispatchers.Default) {
        // CPU-intensive parsing happens off UI thread
        jsonParser.parse(json)
    }

// In ViewModel
fun loadScreen(route: String) {
    viewModelScope.launch {
        _uiState.value = UiState.Loading
        
        val layout = withContext(Dispatchers.Default) {
            parseLayout(api.fetch(route))
        }
        
        _uiState.value = UiState.Success(layout)
    }
}

Batch State Updates

Group multiple state changes into single recomposition:

// Bad: Multiple state updates = multiple recompositions
state.title = "New Title"
state.subtitle = "New Subtitle"
state.imageUrl = "new-image.jpg"

// Good: Single update
state = state.copy(
    title = "New Title",
    subtitle = "New Subtitle",
    imageUrl = "new-image.jpg"
)

5. Lazy Loading & Pagination

Don't load everything at once. Load what's visible.

Above-the-Fold Priority

// Server response with priority hints
{
    "sections": [
        { "id": "header", "priority": "critical", "data": {...} },
        { "id": "hero", "priority": "critical", "data": {...} },
        { "id": "categories", "priority": "high", "data": {...} },
        { "id": "recommendations", "priority": "low", "data": null }
    ]
}

// Client fetches low-priority sections on scroll

Image Lazy Loading

@Composable
fun SDUIImage(url: String, placeholder: Color) {
    AsyncImage(
        model = ImageRequest.Builder(LocalContext.current)
            .data(url)
            .crossfade(true)
            .placeholder(ColorDrawable(placeholder.toArgb()))
            .memoryCachePolicy(CachePolicy.ENABLED)
            .diskCachePolicy(CachePolicy.ENABLED)
            .build(),
        contentDescription = null,
        modifier = Modifier.fillMaxWidth()
    )
}

Infinite Scroll with Cursors

// Initial request
GET /api/layout/feed

// Response includes cursor
{
    "items": [...],
    "nextCursor": "eyJwYWdlIjogMn0="
}

// Load more
GET /api/layout/feed?cursor=eyJwYWdlIjogMn0=

6. Preloading & Prefetching

Anticipate user actions and load before they tap.

Predictive Preloading

// Preload likely next screens
class ScreenPreloader(
    private val cache: SDUICache
) {
    private val predictions = mapOf(
        "/home" to listOf("/search", "/profile", "/cart"),
        "/product/:id" to listOf("/cart", "/checkout"),
        "/cart" to listOf("/checkout")
    )
    
    fun preloadForScreen(currentRoute: String) {
        predictions[currentRoute]?.forEach { route ->
            scope.launch {
                cache.prefetch(route)
            }
        }
    }
}

Prefetch on Hover/Long Press

@Composable
fun SDUILink(
    destination: String,
    preloader: ScreenPreloader
) {
    Box(
        modifier = Modifier
            .pointerInput(Unit) {
                detectTapGestures(
                    onLongPress = {
                        // Prefetch on long press
                        preloader.prefetch(destination)
                    },
                    onTap = {
                        navigator.navigate(destination)
                    }
                )
            }
    )
}

7. Measuring Performance

You can't optimize what you can't measure.

Key Metrics to Track

object SDUIMetrics {
    // Network timing
    fun recordFetchTime(route: String, durationMs: Long, source: String)
    
    // Parse timing
    fun recordParseTime(route: String, durationMs: Long, nodeCount: Int)
    
    // Render timing
    fun recordRenderTime(route: String, durationMs: Long)
    
    // Combined
    fun recordTimeToInteractive(route: String, totalMs: Long)
}

Automated Performance Testing

@Test
fun `home screen renders under 200ms`() = runTest {
    val startTime = System.currentTimeMillis()
    
    val layout = api.fetchLayout("/home")
    composeTestRule.setContent {
        SDUIScreen(layout)
    }
    composeTestRule.waitForIdle()
    
    val duration = System.currentTimeMillis() - startTime
    assertThat(duration).isLessThan(200)
}

8. Real-World Benchmarks

Here's what optimized SDUI performance looks like in production:

Home Screen Load Time (P50)

Unoptimized
850ms
+ Caching
300ms
+ Compression
200ms
+ Preloading
100ms

Production Numbers from Major Apps

Metric Before SDUI After SDUI Optimized SDUI
Time to First Paint 150ms 400ms 80ms
Time to Interactive 200ms 600ms 150ms
Cache Hit Rate N/A 40% 92%
Network Payload N/A 48KB 6KB

โœ… The Result

With proper optimization, SDUI can actually be faster than bundled UI because cached layouts load instantly, and you can update them without app store releases.

Summary: Performance Checklist

Before shipping SDUI to production:

Build Fast SDUI Apps with Pyramid

Pyramid includes built-in caching, compression, and performance monitoring out of the box.

Join the Waitlist โ†’

Further Reading

Related Articles