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.
In This Article
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)
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:
- HTTP/2: Single connection, multiplexed streams
- HTTP/3: QUIC-based, 0-RTT connection setup
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)
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:
- โ Network: Gzip/Brotli compression, CDN edge caching, HTTP/2+
- โ Caching: Multi-layer cache, stale-while-revalidate, ETags
- โ Rendering: Component recycling, memoization, async parsing
- โ Loading: Lazy loading, above-fold priority, infinite scroll
- โ Preloading: Predictive prefetch, hover/focus prefetch
- โ Monitoring: Track TTI, parse time, cache hit rate, scroll FPS
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
- SDUI Architecture Patterns: Building Scalable Server-Driven UI Systems โ
- Server-Driven UI Security Best Practices โ
- Server-Driven UI Tutorial: Building Dynamic Screens with SwiftUI โ
- Server-Driven UI for Android: A Complete Implementation Guide โ
- Getting Started with Pyramid: Add Server-Driven UI to Your Android App in 30 Minutes โ