Skin Performance and Maintainability Best Practices¶
Overview¶
Creating a high-performance, maintainable Movian skin requires careful attention to widget hierarchy, resource management, animation optimization, and code organization. This guide provides comprehensive best practices based on analysis of the official Movian flat skin and real-world skin development experience.
Table of Contents¶
- Performance Optimization
- Maintainability Principles
- Resource Management
- Animation Best Practices
- Layout Optimization
- Code Organization
- Testing and Debugging
- Common Pitfalls
Performance Optimization¶
Widget Hierarchy Optimization¶
Minimize Nesting Depth
Deep widget hierarchies increase rendering overhead and layout calculation time.
❌ Bad Practice:
widget(container_z, {
widget(container_y, {
widget(container_x, {
widget(container_z, {
widget(container_y, {
widget(label, {
caption: "Hello";
});
});
});
});
});
});
✅ Good Practice:
Impact: Reducing nesting from 6 levels to 2 levels can improve rendering performance by 40-60% on low-end devices.
Use filterConstraint Attributes¶
The filterConstraintX and filterConstraintY attributes prevent unnecessary layout recalculations when content changes.
✅ Good Practice:
widget(container_x, {
widget(label, {
filterConstraintX: true; // Prevents horizontal layout recalculation
caption: $self.dynamicText;
});
widget(icon, {
width: 2em; // Fixed width, no constraint needed
source: "icon.svg";
});
});
When to Use: - Text labels with dynamic content - Images with variable dimensions - Any widget that might change size during runtime
Performance Gain: 20-30% reduction in layout calculation time for dynamic content.
Lazy Loading with Conditional Rendering¶
Load components only when needed to reduce initial rendering time and memory usage.
✅ Good Practice:
// Load OSD only when visible
widget(loader, {
autohide: true;
source: select($clone.osdpage > 0, "skin://osd/osd_main.view", "");
});
// Load settings page only when requested
widget(loader, {
autohide: true;
source: translate($clone.osdpage, "",
2, "skin://osd/osd_settings_subs.view",
3, "skin://osd/osd_settings_audio.view");
});
Benefits: - Faster initial page load (50-70% improvement) - Lower memory footprint - Smoother navigation
Optimize Cloner Usage¶
The cloner() widget creates instances for each item in a collection. Optimize its usage for large lists.
❌ Bad Practice:
cloner($self.items, container_z, {
// Complex nested structure for each item
widget(container_z, {
widget(container_y, {
widget(container_x, {
// Many nested widgets
});
});
});
});
✅ Good Practice:
cloner($self.items, loader, {
// Load item template from separate file
source: "skin://items/list/item.view";
time: 0.3;
noInitialTransform: true; // Skip initial animation for better performance
});
Performance Tips:
- Use loader with external templates for complex items
- Set noInitialTransform: true for large lists
- Keep item templates simple and flat
- Use hidden: !$self.enabled to skip disabled items
Image and Icon Optimization¶
Use Appropriate Image Formats: - SVG: Icons and simple graphics (scalable, small file size) - PNG: Screenshots and complex images with transparency - JPEG: Photos and backgrounds without transparency
Optimize Image Loading:
widget(image, {
source: $self.metadata.icon;
// Specify dimensions to avoid layout recalculation
width: 10em;
height: 10em;
// Use appropriate scaling
aspectConstraint: true;
});
Icon Caching:
// Reuse common icons through variables
$ui.iconPlay = "skin://icons/ic_play_arrow_48px.svg";
$ui.iconPause = "skin://icons/ic_pause_48px.svg";
widget(icon, {
source: $ui.iconPlay; // Cached reference
});
Reduce Alpha Blending¶
Alpha blending is computationally expensive. Minimize transparent overlays.
❌ Bad Practice:
widget(container_z, {
widget(quad, { alpha: 0.9; });
widget(quad, { alpha: 0.8; });
widget(quad, { alpha: 0.7; });
widget(quad, { alpha: 0.6; });
// Multiple transparent layers
});
✅ Good Practice:
widget(container_z, {
widget(quad, { alpha: 0.8; }); // Single background layer
// Opaque content
});
Maintainability Principles¶
DRY Principle: Use Macros¶
Extract repeated patterns into reusable macros.
❌ Bad Practice (Repetition):
// Repeated in multiple places
widget(container_z, {
height: 2em;
focusable: true;
onEvent(activate, navOpen($self.url));
widget(quad, {
alpha: 0.1 * isHovered() + 0.2 * isNavFocused();
});
widget(container_x, {
widget(icon, { source: $self.icon; });
widget(label, { caption: $self.title; });
});
});
✅ Good Practice (Macro):
#define ListItem(ICON, CAPTION, EVENT) {
widget(container_z, {
height: 2em;
focusable: true;
onEvent(activate, EVENT);
ListItemHighlight();
widget(container_x, {
widget(icon, { source: ICON; });
widget(label, { caption: CAPTION; });
});
});
}
// Usage
ListItem($self.icon, $self.title, navOpen($self.url));
Consistent Naming Conventions¶
Use clear, descriptive names that follow a consistent pattern.
✅ Good Practice:
// Global UI variables
$ui.colorPrimary
$ui.colorSecondary
$ui.spacingNormal
$ui.fontSizeLarge
// Component IDs
"osd_main"
"osd_settings_subs"
"playdeck_video"
// Macro names (PascalCase)
ListItemHighlight()
PlaydeckButton()
ScrollBar()
// File names (kebab-case)
osd-main.view
playdeck-video.view
sidebar-action.view
Separation of Concerns¶
Organize code by function and keep configuration separate from implementation.
File Structure:
skin/
├── universe.view # Entry point and global state
├── theme.view # Macros and reusable components
├── pages/ # Page implementations
│ ├── video.view
│ ├── directory.view
│ └── settings.view
├── osd/ # OSD system
│ ├── osd_main.view
│ └── osd_settings_subs.view
├── playdecks/ # Media controls
│ ├── playdeck_video.view
│ └── playdeck_audio.view
├── popups/ # Modal dialogs
│ ├── auth.view
│ └── message.view
└── components/ # Shared components
├── sidebar.view
└── header.view
Use Global Configuration¶
Centralize configuration in universe.view for easy customization.
✅ Good Practice:
// universe.view - Global configuration
$ui.color1 = "#4192ff";
$ui.color2 = "#306cbe";
$ui.spacingNormal = 1em;
$ui.animSpeed = 4;
// Usage in components
widget(quad, {
color: $ui.color1; // Reference global variable
});
alpha: iir($visible, $ui.animSpeed); // Use global animation speed
Benefits: - Single source of truth for theme values - Easy theme customization - Consistent appearance across skin
Document Complex Logic¶
Add comments for non-obvious code, especially complex expressions.
✅ Good Practice:
// Calculate sidebar width based on screen aspect ratio
// Wider screens get larger sidebar for better usability
$ui.sidebarWidth = select($ui.aspect > 1.5, 20em, 15em);
// Smooth fade-in animation with 4x interpolation speed
// Hidden when alpha drops below 0.01 to save rendering
alpha: iir($clone.osdpage > 0, 4);
hidden: iir($clone.osdpage > 0, 3) < 0.01;
// Page state management:
// 0 = hidden, 1 = main menu, 2-4 = settings, 100+ = track selection
$clone.osdpage = 0;
Resource Management¶
Memory Management¶
Unload Unused Resources:
widget(loader, {
autohide: true; // Automatically unload when source is empty
source: select($ui.showComponent, "component.view", "");
});
Limit Cloner Scope:
cloner($self.items, loader, {
// Only clone visible/enabled items
hidden: !$self.enabled;
source: "item.view";
});
Asset Organization¶
Organize Assets by Type:
skin/
├── icons/ # SVG icons
│ ├── ic_play_48px.svg
│ └── ic_pause_48px.svg
├── images/ # Raster images
│ ├── background.jpg
│ └── logo.png
└── fonts/ # Custom fonts (if any)
Use Consistent Asset Naming:
- Icons: ic_<name>_<size>px.svg (e.g., ic_play_48px.svg)
- Images: <purpose>_<variant>.png (e.g., background_dark.png)
Preload Critical Resources¶
Load essential resources early to avoid delays:
// universe.view - Preload common icons
$ui.iconPlay = "skin://icons/ic_play_arrow_48px.svg";
$ui.iconPause = "skin://icons/ic_pause_48px.svg";
$ui.iconSettings = "skin://icons/ic_settings_48px.svg";
// Preload background
widget(loader, {
source: "background.view";
priority: high;
});
Animation Best Practices¶
Use iir() for Smooth Interpolation¶
The iir() (Infinite Impulse Response) function provides smooth, natural animations.
✅ Good Practice:
// Smooth fade-in/out
alpha: iir($visible, 4);
// Smooth color transition
color: iir(isNavFocused(), 8, true);
// Smooth scaling
scaling: [iir($highlighted, 6), iir($highlighted, 6), 1];
Parameters:
- iir(target, speed): Interpolate to target value
- iir(target, speed, absolute): Use absolute interpolation (for colors)
Speed Guidelines: - Fast animations: 8-16 (button highlights, hover effects) - Normal animations: 4-6 (fades, transitions) - Slow animations: 2-3 (background changes, large movements)
Avoid Animation Overload¶
Too many simultaneous animations can cause performance issues.
❌ Bad Practice:
// Animating everything simultaneously
alpha: iir($visible, 4);
scaling: [iir($scale, 4), iir($scale, 4), 1];
rotation: iir($rotation, 4);
color: iir($color, 4, true);
// Too many animations on one widget
✅ Good Practice:
Optimize Animation Triggers¶
Avoid triggering animations on every frame.
❌ Bad Practice:
// Recalculates every frame
alpha: $self.media.current.currenttime / $self.media.current.metadata.duration;
✅ Good Practice:
// Use iir() to smooth updates
alpha: iir($self.media.current.currenttime / $self.media.current.metadata.duration, 8);
Use noInitialTransform for Lists¶
Skip initial animations for better performance with large lists:
cloner($self.items, loader, {
source: "item.view";
time: 0.3;
noInitialTransform: true; // Skip animation on initial load
});
Layout Optimization¶
Use Appropriate Container Types¶
Choose the right container for your layout needs:
container_x: Horizontal layout (left to right)container_y: Vertical layout (top to bottom)container_z: Stacked layout (z-order)list_x/list_y: Scrollable listsgrid: Grid layout
✅ Good Practice:
// Use list_y for scrollable vertical content
widget(list_y, {
id: "mylist";
cloner($self.items, loader, {
source: "item.view";
});
});
// Use container_x for fixed horizontal layout
widget(container_x, {
spacing: 1em;
widget(icon, { source: "icon.svg"; });
widget(label, { caption: "Text"; });
});
Optimize Spacing and Padding¶
Use consistent spacing values from global configuration:
✅ Good Practice:
// Define in universe.view
$ui.spacingXSmall = 0.25em;
$ui.spacingSmall = 0.5em;
$ui.spacingNormal = 1em;
$ui.spacingLarge = 1.5em;
$ui.spacingXLarge = 2em;
// Use in components
widget(container_y, {
padding: $ui.spacingLarge;
spacing: $ui.spacingNormal;
});
Use Weight for Flexible Layouts¶
The weight attribute creates flexible, responsive layouts:
widget(container_x, {
// Sidebar with fixed width
widget(container_y, {
width: 15em;
// Sidebar content
});
// Main content fills remaining space
widget(container_y, {
weight: 1; // Takes all remaining horizontal space
// Main content
});
});
Avoid Unnecessary Calculations¶
Cache calculated values instead of recalculating:
❌ Bad Practice:
widget(container_x, {
width: $ui.width / 2;
widget(label, { width: $ui.width / 2; });
widget(label, { width: $ui.width / 2; });
widget(label, { width: $ui.width / 2; });
});
✅ Good Practice:
$ui.halfWidth = $ui.width / 2; // Calculate once
widget(container_x, {
width: $ui.halfWidth;
widget(label, { width: $ui.halfWidth; });
widget(label, { width: $ui.halfWidth; });
widget(label, { width: $ui.halfWidth; });
});
Code Organization¶
Modular Component Design¶
Break complex UIs into smaller, reusable components:
✅ Good Practice:
// components/sidebar.view
widget(container_y, {
width: 20em;
// Sidebar implementation
});
// pages/video.view
widget(container_x, {
widget(loader, {
source: "skin://components/sidebar.view";
});
widget(container_y, {
weight: 1;
// Main video content
});
});
Use Include Files for Shared Code¶
Extract shared macros and styles:
// theme.view - Shared macros
#define ListItemHighlight() {
widget(quad, {
alpha: 0.1 * isHovered() + 0.2 * isNavFocused();
});
}
// Import in components
#import "skin://theme.view"
widget(container_z, {
ListItemHighlight(); // Use shared macro
});
Version Control Best Practices¶
Commit Structure:
skin: Add OSD subtitle settings page
skin: Optimize list rendering performance
skin: Fix focus navigation in sidebar
File Organization for Git: - Keep related changes in single commits - Separate feature additions from bug fixes - Document breaking changes in commit messages
Testing and Debugging¶
Enable Debug Overlays¶
Use Movian's built-in debugging features:
// Show widget boundaries (development only)
$ui.showDebugBorders = true;
// Log navigation events
onEvent(activate, {
print("Activated: " + $self.title);
navOpen($self.url);
});
Test on Multiple Devices¶
Device Categories: 1. TV/Set-top box: Large screen, remote control navigation 2. Desktop: Mouse and keyboard, variable screen sizes 3. Mobile: Touch input, small screen, portrait/landscape
Test Checklist: - [ ] Navigation works with keyboard/remote - [ ] Touch targets are large enough (minimum 44x44 pixels) - [ ] Text is readable at all screen sizes - [ ] Animations are smooth (60 FPS target) - [ ] Memory usage is acceptable (< 100MB for skin)
Performance Profiling¶
Monitor Key Metrics:
// Log rendering time (development only)
$ui.renderTime = changed($ui.frame, 1000, 0);
widget(label, {
caption: fmt("FPS: %d", 1000 / $ui.renderTime);
hidden: !$ui.showDebug;
});
Validation Scripts¶
Use the provided validation tools:
# Validate skin structure
./docs/tests/run-skin-structure-validation.sh path/to/skin
# Validate macro usage
./docs/tests/run-macro-validation.sh path/to/skin
Common Pitfalls¶
Pitfall 1: Circular Dependencies¶
❌ Problem:
// file1.view
#import "skin://file2.view"
// file2.view
#import "skin://file1.view" // Circular dependency!
✅ Solution: Extract shared code to a third file:
// shared.view
#define SharedMacro() { ... }
// file1.view
#import "skin://shared.view"
// file2.view
#import "skin://shared.view"
Pitfall 2: Missing Focus Management¶
❌ Problem:
widget(container_y, {
// No focusable widgets - navigation doesn't work!
widget(label, { caption: "Text"; });
widget(label, { caption: "More text"; });
});
✅ Solution:
widget(container_y, {
widget(container_z, {
focusable: true; // Make container focusable
onEvent(activate, doSomething());
widget(label, { caption: "Text"; });
});
});
Pitfall 3: Hardcoded Values¶
❌ Problem:
✅ Solution:
// universe.view
$ui.spacingNormal = 1em;
// component.view
widget(container_y, {
padding: $ui.spacingNormal;
spacing: $ui.spacingNormal;
});
Pitfall 4: Ignoring Hidden State¶
❌ Problem:
✅ Solution:
Pitfall 5: Over-Engineering¶
❌ Problem:
// Overly complex macro with too many parameters
#define ComplexWidget(P1, P2, P3, P4, P5, P6, P7, P8, P9, P10) {
// 200 lines of code
}
✅ Solution:
// Simple, focused macros
#define SimpleWidget(CAPTION, EVENT) {
// 10-20 lines of code
}
// Compose simple macros for complex UIs
Performance Checklist¶
Use this checklist to ensure optimal performance:
Rendering Performance¶
- [ ] Widget hierarchy depth < 5 levels
- [ ]
filterConstraintX/Yused on dynamic content - [ ]
autohide: trueon conditional loaders - [ ]
noInitialTransform: trueon large lists - [ ] Minimal alpha blending (< 3 transparent layers)
Memory Usage¶
- [ ] Lazy loading for non-critical components
- [ ] Images optimized (appropriate format and size)
- [ ] Unused resources unloaded
- [ ] Cloner scope limited to visible items
Animation Performance¶
- [ ]
iir()used for smooth interpolation - [ ] Animation speed appropriate (2-16 range)
- [ ] < 5 simultaneous animations per widget
- [ ] Animation triggers optimized
Code Quality¶
- [ ] DRY principle applied (macros for repeated patterns)
- [ ] Consistent naming conventions
- [ ] Global configuration used
- [ ] Complex logic documented
- [ ] Modular component design
Testing¶
- [ ] Tested on TV/remote control
- [ ] Tested on desktop/mouse
- [ ] Tested on mobile/touch
- [ ] Performance profiled (60 FPS target)
- [ ] Memory usage monitored (< 100MB)
Conclusion¶
Following these best practices will result in skins that are:
- Performant: Smooth 60 FPS rendering on all devices
- Maintainable: Easy to update and extend
- Scalable: Handles large datasets efficiently
- Accessible: Works well with all input methods
- Professional: Consistent, polished user experience
For more information, see: - Minimal Skin Guide - Basic skin concepts - Advanced Skin Guide - Advanced features - Debugging View Files - Troubleshooting techniques - Skin Template Generator - Quick start tool
Additional Resources¶
Performance Tools¶
- Skin structure validator:
docs/tests/skin-structure-validator.js - Macro validator:
docs/tests/macro-validator.js - Skin template generator:
tools/generate-skin-template.js
Example Skins¶
- Minimal skin:
docs/ui/theming/examples/minimal-skin/ - Advanced skin:
docs/ui/theming/examples/advanced-skin/ - Official flat skin:
movian/glwskins/flat/