Last Updated: February 2026 Version: 1.0.0
This guide helps you optimize HyperRender performance for different use cases.
- Quick Performance Checklist
- Render Modes Explained
- Choosing the Right Mode
- Image Loading & Caching
- CSS Optimization
- Memory Management
- Profiling with DevTools
- Common Performance Issues
DO:
- Use HyperRenderMode.sync for small content
- Use HyperRenderMode.virtualized for large content
- Use HyperRenderMode.auto when content size varies
- Cache parsed DocumentNodes when content doesn't change
- Minimize inline styles (use CSS classes instead)
- Use selectable: false if selection not needed
- Optimize images (compress, resize before rendering)
DON'T:
- Parse the same HTML repeatedly
- Use virtualized mode for short content (overhead not worth it)
- Load huge images without constraints
- Use complex CSS selectors unnecessarily
- Enable zoom on virtualized content (conflicts with scrolling)
HyperRender offers 3 render modes optimized for different scenarios:
How it works:
- Parses HTML on main thread (synchronous)
- Renders entire content as single widget
- Scrollable via SingleChildScrollView
Performance:
- Parse time: Fast for small content
- First frame: Very fast (single layout pass)
- Memory: Low (single RenderObject)
Best for:
HyperViewer(
html: shortArticle,
mode: HyperRenderMode.sync,
)Use cases:
- Comments/posts
- Email previews
- Chat messages with formatting
- Product descriptions
How it works:
- Parses HTML in background isolate (async)
- Splits content into chunks
- Renders via ListView.builder (virtualizes viewport)
- Only visible sections are in memory
Performance:
- Parse time: Longer but doesn't block UI
- First frame: Fast (renders above-fold content first)
- Memory: Efficient (only viewport + cache rendered)
- Scroll: Smooth even with large HTML
Best for:
HyperViewer(
html: longDocument,
mode: HyperRenderMode.virtualized,
)Use cases:
- Long articles/blog posts
- Documentation pages
- Books/novels
- News feeds
How it works:
- Checks content length
- Uses sync for smaller content
- Uses virtualized for larger content
Best for:
HyperViewer(
html: variableLengthContent,
mode: HyperRenderMode.auto,
)Use cases:
- User-generated content (variable length)
- API responses (unknown size)
- Mixed content (comments + articles)
Content Length?
├─ Small content
│ └─ Use: sync (fastest first render)
│
├─ Medium content
│ └─ Use: auto (will choose sync)
│
├─ Large content
│ └─ Use: virtualized (smooth scrolling)
│
└─ Very large content
└─ Use: virtualized (only viable option)
| Mode | Parse Time | Memory Usage | Scroll Performance |
|---|---|---|---|
| sync | Fast (blocking) | Higher (all rendered) | Good |
| virtualized | Slower (non-blocking) | Lower (viewport only) | Smooth |
| auto | Adaptive | Adaptive | Good |
HyperRender uses Flutter's built-in image cache:
// Default cache settings use Flutter's defaults
// - Eviction: LRU (Least Recently Used)// In main.dart, before runApp()
void main() {
// Adjust cache size for image-heavy content if needed
PaintingBinding.instance.imageCache.maximumSize = 2000;
PaintingBinding.instance.imageCache.maximumSizeBytes = 200 * 1024 * 1024;
runApp(MyApp());
}HyperViewer(
html: content,
widgetBuilder: (context, node) {
if (node is AtomicNode && node.tagName == 'img') {
final src = node.src;
if (src == null) return null;
// Use CachedNetworkImage for better performance
return CachedNetworkImage(
imageUrl: src,
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
// Add memory cache
memCacheWidth: 800,
maxWidthDiskCache: 1200,
);
}
return null;
},
)// Good - Constrain image sizes
HyperViewer(
html: '''
<img src="huge-image.jpg" style="max-width: 100%; height: auto;">
''',
)
// Bad - Unconstrained images may cause memory issues
HyperViewer(
html: '''
<img src="huge-image.jpg">
''',
)HyperRender uses indexed lookup for CSS rules:
// Internal implementation:
// - Class rules indexed by class name
// - ID rules indexed by ID
// - Tag rules indexed by tag name
// Result: Fast lookup performancePrefer classes over complex selectors:
<!-- Fast: Direct class lookup -->
<div class="card">Content</div>
<!-- Slower: Must traverse tree -->
<div>
<div>
<div class="deep-nested">Content</div>
</div>
</div>Minimize inline styles:
<!-- Better: Reuses computed style -->
<p class="text">Paragraph 1</p>
<p class="text">Paragraph 2</p>
<p class="text">Paragraph 3</p>
<!-- Worse: Computes style multiple times -->
<p style="color: red; font-size: 16px;">Paragraph 1</p>
<p style="color: red; font-size: 16px;">Paragraph 2</p>
<p style="color: red; font-size: 16px;">Paragraph 3</p>Use CSS shorthand:
/* Better: Single property parse */
.box {
margin: 16px 8px;
padding: 12px;
}
/* Worse: Multiple property parses */
.box {
margin-top: 16px;
margin-right: 8px;
margin-bottom: 16px;
margin-left: 8px;
}HyperRender uses LRU cache for TextPainters:
// Uses LRU cache with automatic eviction
// Eviction: Least Recently Used (LRU)
// Each evicted painter is properly disposed
// No configuration needed - handled automatically// Automatic disposal
// All TapGestureRecognizers are disposed when widget is removed
// Implemented in RenderHyperBox.dispose()
// No manual cleanup neededCheck memory usage with DevTools:
# Run app in profile mode
flutter run --profile
# Open DevTools
# Memory tab → Take snapshot
# Look for:
# - Image cache size
# - TextPainter count
# - GestureRecognizer countMeasure render time:
final stopwatch = Stopwatch()..start();
HyperViewer(
html: content,
placeholderBuilder: (context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Parsing... ${stopwatch.elapsedMilliseconds}ms'),
],
),
);
},
)# Run in profile mode
flutter run --profile
# Open DevTools → Performance tab
# Look for:
# - Frame rendering time (should be < 16ms for 60fps)
# - Jank (red bars indicate dropped frames)
# - Build/Layout/Paint timesIdentify bottlenecks:
- Open DevTools → CPU Profiler
- Click "Record"
- Scroll through content
- Click "Stop"
- Analyze flame graph:
- Look for wide bars (time-consuming operations)
- Common culprits: image decoding, layout, paint
Detect memory leaks:
# Steps:
1. Open DevTools → Memory tab
2. Take baseline snapshot
3. Navigate to HyperViewer screen
4. Take snapshot #2
5. Navigate away
6. Take snapshot #3
7. Compare snapshots:
- Memory should return to baseline after navigation
- If not, investigate retained objectsSymptom: Stuttering/dropped frames while scrolling
Causes:
- Large inline images not constrained
- Too many TextPainters being created
- Complex CSS selectors
Solutions:
// Constrain image sizes
<img src="image.jpg" style="max-width: 100%; height: auto;">
// Use virtualized mode for large content
HyperViewer(
html: largeContent,
mode: HyperRenderMode.virtualized,
)Symptom: Long delay before content appears
Causes:
- Parsing large HTML on main thread
- Loading many images synchronously
Solutions:
// Use virtualized mode for async parsing
HyperViewer(
html: largeContent,
mode: HyperRenderMode.virtualized,
)
// Show loading indicator
HyperViewer(
html: content,
placeholderBuilder: (context) => CircularProgressIndicator(),
)
// Lazy-load images
widgetBuilder: (context, node) {
if (node is AtomicNode && node.isImage) {
return CachedNetworkImage(
imageUrl: node.src!,
placeholder: (context, url) => Shimmer(...),
);
}
return null;
}Symptom: App using excessive RAM
Causes:
- Large uncompressed images
- Too many cached images
- Not using virtualized mode for large content
Solutions:
// Reduce image cache size if needed
PaintingBinding.instance.imageCache.maximumSizeBytes = 50 * 1024 * 1024;
// Use virtualized mode
HyperViewer(
html: largeContent,
mode: HyperRenderMode.virtualized,
)
// Compress images server-side
// - Use WebP format
// - Resize to display sizeSymptom: Long layout times in DevTools
Causes:
- Too many CSS rules
- Complex descendant selectors
Solutions:
// Use specific classes
<style>
.article-text { color: #333; }
</style>
<p class="article-text">Content</p>
// Avoid complex selectors
<style>
div > section > article > p { color: #333; }
</style>HyperViewer(
html: shortContent,
mode: HyperRenderMode.sync,
selectable: true,
)HyperViewer(
html: mediumContent,
mode: HyperRenderMode.auto,
placeholderBuilder: (context) => CircularProgressIndicator(),
)HyperViewer(
html: largeContent,
mode: HyperRenderMode.virtualized,
selectable: false,
placeholderBuilder: (context) => LoadingShimmer(),
)HyperViewer(
html: content,
widgetBuilder: (context, node) {
if (node is AtomicNode && node.isImage) {
return CachedNetworkImage(
imageUrl: node.src!,
memCacheWidth: 800,
placeholder: (context, url) => Shimmer(...),
);
}
return null;
},
)class PerformanceMonitor {
static void trackRenderTime(String contentId, Duration duration) {
// Send to analytics
FirebaseAnalytics.instance.logEvent(
name: 'hyper_render_performance',
parameters: {
'content_id': contentId,
'render_time_ms': duration.inMilliseconds,
'content_length': html.length,
},
);
}
}
// Usage:
final stopwatch = Stopwatch()..start();
HyperViewer(
html: content,
// ... after rendering
);
PerformanceMonitor.trackRenderTime('article_123', stopwatch.elapsed);- Flutter Performance Best Practices: https://docs.flutter.dev/perf/best-practices
- DevTools User Guide: https://docs.flutter.dev/tools/devtools
- Image Optimization: https://docs.flutter.dev/perf/rendering-performance#images
- HyperRender Benchmarks: benchmark/ folder
Remember: Profile first, optimize second. Use DevTools to identify actual bottlenecks before making changes.