@lightningjs/blits 1.29.5 → 1.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ _24 jun 2025_
4
+
5
+ ## v1.30.0
6
+
7
+ - Added documentation on lazy loading performance
8
+ - Bumped renderer to v2.14.0 (simplified texture throttling)
9
+ - Fixed bug in announcer when utterance is cancelled
10
+
11
+
3
12
  _12 jun 2025_
4
13
 
5
14
  ## v1.29.5
package/docs/_sidebar.md CHANGED
@@ -33,3 +33,5 @@
33
33
  - [Language](/plugins/language.md)
34
34
  - [Theme](/plugins/theme.md)
35
35
  - [Global App State](/plugins/global_app_state.md)
36
+ - Performance
37
+ - [Lazy loading]('/performance/lazy-loading.md')
@@ -0,0 +1,161 @@
1
+ # Lazy Loading with the :range Parameter
2
+
3
+ The for-loop in Blits comes with a [`:range` parameter](../built-in/for-loop.html#using-the-range-attribute) that allows you to render only a portion of an array rather than all the items.
4
+
5
+ This `:range` parameter can also be used to easily implement an efficient form of _lazy loading_. Lazy loading is a crucial concept for performance in TV applications where rails can contain hundreds of items, but only a small subset is visible at any time.
6
+
7
+ ## Basic range implementation
8
+
9
+ For the basic syntax of the `:range` attribute, we typically set dynamic `from` and `to` values, which define the subset of items to display.
10
+
11
+ ```js
12
+ <Tile :for="(item, index) in $items" :range="{from: $from, to: $to}" />
13
+ ```
14
+
15
+ A full example of a Rail component with a range would look something like this:
16
+
17
+ ```js
18
+ export default Blits.Component('RangedRail', {
19
+ template: `
20
+ <Element :x.transition="0 - $focused * 300">
21
+ <Tile
22
+ :for="(item, index) in $items"
23
+ :range="{from: $range, to: $range + 7}"
24
+ :key="$item.id"
25
+ :focused="$focused"
26
+ w="300"
27
+ h="200"
28
+ :x="$index * 320"
29
+ />
30
+ </Element>
31
+ `,
32
+ state() {
33
+ return {
34
+ range: 0,
35
+ focused: 0,
36
+ items: [] // Array of 100+ items
37
+ }
38
+ },
39
+ input: {
40
+ right() {
41
+ this.focus = Math.min(this.focus + 1, items.length)
42
+ this.range++
43
+ },
44
+ left() {
45
+ this.focus = Math.max(this.focus - 1, 0)
46
+ this.range--
47
+ }
48
+ }
49
+ })
50
+ ```
51
+
52
+ Benefits of this approach are:
53
+
54
+ - **Selective Rendering**: Only items within the specified range are actually created as elements
55
+ - **Performance Optimization**: Reduces memory usage and improves rendering performance
56
+ - **Dynamic Range Updates**: The range can be computed based on user interaction and scroll position
57
+
58
+ A downside of this approach is that, as we scroll we are also **destroying** components that are out of range. And we may need those
59
+ same items when we scroll back (and thus recreate them). On top of that, destroying Components and Elements also comes at a cost.
60
+
61
+ ## Lazy loading with the range attribute
62
+
63
+ We can solve the problem of destroying Components that we may want to use again by slightly changing the logic of the Rail component.
64
+
65
+ 1) We set `from: 0`, to ensure we always start from the first item (and _keep_ the first items)
66
+ 2) We use `to: $range + 7` to render up to the current position plus a lookahead buffer
67
+ 3) The `$range` value tracks how far the user has navigated through the rail, but it doesn't decrement when navigating back
68
+
69
+ With these changes a full example of a LazyRail component would look something like this:
70
+
71
+ ```js
72
+ export default Blits.Component('LazyRail', {
73
+ template: `
74
+ <Element :x.transition="0 - $focused * 300">
75
+ <Tile
76
+ :for="(item, index) in $items"
77
+ :range="{from: 0, to: $range + 7}"
78
+ :key="$item.id"
79
+ w="300"
80
+ h="200"
81
+ :x="$index * 320"
82
+ />
83
+ </Element>
84
+ `,
85
+ state() {
86
+ return {
87
+ range: 0,
88
+ items: [] // Array of 100+ items
89
+ }
90
+ },
91
+ input: {
92
+ right() {
93
+ this.focus = Math.min(this.focus + 1, items.length)
94
+ // push the range update to the next tick to spread out the work
95
+ // and ease the CPU a bit
96
+ this.$setTimeout(() => this.range = this.focus)
97
+
98
+ },
99
+ left() {
100
+ this.focus = Math.max(this.focus - 1, 0)
101
+ // we don't decrement the range when scrolling back to keep components alive
102
+ // this.range--
103
+ }
104
+ }
105
+ })
106
+ ```
107
+
108
+ ## Performance Benefits
109
+
110
+ Properly applying the range attribute provides **Memory Efficiency**:
111
+
112
+ - Instead of creating 100+ elements, only ~8-15 are active
113
+ - Reduces GPU memory usage on constrained TV hardware
114
+ - Lower overall memory footprint
115
+
116
+ It also is beneficial for **Faster Rendering**
117
+
118
+ - Initial page load only renders visible items
119
+ - Scroll performance improves as fewer elements need updates
120
+ - Reduced layout calculations
121
+
122
+
123
+ It **optimizes bandwidth**
124
+
125
+ - Images and assets only load for rendered items
126
+ - Reduces network requests on page load
127
+ - Progressive content loading
128
+
129
+
130
+ ### Alpha Transitions
131
+
132
+ For a smoother experience you consider applying an alpha transition, that fades out items at the end of the rang.e
133
+
134
+ ```js
135
+ :alpha.transition="$index <= $range ? 1 : ($index <= $range + 3 ? 0.8 : 0.6)"
136
+ ```
137
+
138
+ This creates a dimming effect for previously viewed items while maintaining full opacity for the current and upcoming items.
139
+
140
+ ## Best Practices
141
+
142
+ ### Buffer Size
143
+
144
+ Use a buffer of 5-10 items ahead of current position:
145
+
146
+ ```js
147
+ // Conservative buffer for low-end devices
148
+ :range="{from: $currentIndex - 1, to: $currentIndex + 6}"
149
+
150
+ // Larger buffer for smoother experience
151
+ :range="{from: $currentIndex - 2, to: $currentIndex + 10}"
152
+ ```
153
+
154
+ ## Performance Impact
155
+
156
+ On typical TV hardware:
157
+
158
+ - **Memory Usage**: 70-80% reduction compared to full rendering
159
+ - **Initial Load Time**: 3-5x faster page initialization
160
+ - **Scroll Performance**: Consistent fps maintained vs potential frame drops
161
+ - **Network Requests**: Reduced by 80-90% on initial load
package/docs/sidebar.json CHANGED
@@ -112,6 +112,15 @@
112
112
  }
113
113
  ]
114
114
  },
115
+ {
116
+ "text": "Performance",
117
+ "items": [
118
+ {
119
+ "text": "Lazy Loading",
120
+ "link": "/performance/lazy-loading"
121
+ }
122
+ ]
123
+ },
115
124
  {
116
125
  "text": "Router",
117
126
  "items": [
@@ -149,5 +158,14 @@
149
158
  "link": "/plugins/global_app_state"
150
159
  }
151
160
  ]
161
+ },
162
+ {
163
+ "text": "Performance",
164
+ "items": [
165
+ {
166
+ "text": "Lazy loading",
167
+ "link": "/performance/lazy-loading"
168
+ }
169
+ ]
152
170
  }
153
171
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "1.29.5",
3
+ "version": "1.30.0",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@lightningjs/msdf-generator": "^1.1.1",
50
- "@lightningjs/renderer": "^2.13.4"
50
+ "@lightningjs/renderer": "^2.14.0"
51
51
  },
52
52
  "repository": {
53
53
  "type": "git",
@@ -70,6 +70,7 @@ const addToQueue = (message, politeness, delay = false) => {
70
70
  done.cancel = () => {
71
71
  const index = queue.findIndex((item) => item.id === id)
72
72
  if (index !== -1) queue.splice(index, 1)
73
+ isProcessing = false
73
74
  resolveFn('canceled')
74
75
  }
75
76