rune-scroller 2.1.0 → 2.2.1
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/README.md +186 -85
- package/dist/dom-utils.d.ts +1 -0
- package/dist/dom-utils.js +19 -4
- package/dist/index.js +3 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,10 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
**📚 Complete API Reference** — Detailed documentation for all features and options.
|
|
4
4
|
|
|
5
|
-
**Quick start?** See [README.md](../README.md) for a simpler introduction.
|
|
6
|
-
|
|
7
|
-
**Development?** See [CLAUDE.md](../CLAUDE.md) for the developer guide.
|
|
8
|
-
|
|
9
5
|
---
|
|
10
6
|
|
|
11
7
|
<div align="center">
|
|
@@ -30,7 +26,7 @@
|
|
|
30
26
|
|
|
31
27
|
## ✨ Features
|
|
32
28
|
|
|
33
|
-
- **
|
|
29
|
+
- **14KB gzipped** (47KB uncompressed) - Minimal overhead, optimized for production
|
|
34
30
|
- **Zero dependencies** - Pure Svelte 5 + IntersectionObserver
|
|
35
31
|
- **14 animations** - Fade, Zoom, Flip, Slide, Bounce variants
|
|
36
32
|
- **Full TypeScript support** - Type definitions generated from JSDoc
|
|
@@ -40,43 +36,159 @@
|
|
|
40
36
|
- **v2.0.0 New** - `onVisible` callback, ResizeObserver support, animation validation, sentinel customization
|
|
41
37
|
- **✨ Latest** - `useIntersection` migrated to Svelte 5 `$effect` rune for better lifecycle management
|
|
42
38
|
- **🚀 Bundle optimized** - CSS with custom properties, production build minification
|
|
39
|
+
- **🚀 v2.2.0** - Cache CSS check to eliminate 99% of reflows
|
|
43
40
|
|
|
44
41
|
---
|
|
42
|
+
|
|
43
|
+
## 🚀 Performance
|
|
44
|
+
|
|
45
|
+
### Cache CSS Validation (v2.2.0)
|
|
46
|
+
|
|
47
|
+
**Problem:**
|
|
48
|
+
- `checkAndWarnIfCSSNotLoaded()` was called for EVERY element
|
|
49
|
+
- Each call did:
|
|
50
|
+
- `document.createElement('div')`
|
|
51
|
+
- `document.body.appendChild(test)`
|
|
52
|
+
- `getComputedStyle(test)` ⚠️ **Expensive!** Forces full page reflow
|
|
53
|
+
- `test.remove()`
|
|
54
|
+
- For 100 animated elements = **100 reflows + 100 DOM operations**
|
|
55
|
+
|
|
56
|
+
**Solution:**
|
|
57
|
+
```javascript
|
|
58
|
+
// Cache to check only once per page load
|
|
59
|
+
let cssCheckResult = null;
|
|
60
|
+
|
|
61
|
+
export function checkAndWarnIfCSSNotLoaded() {
|
|
62
|
+
if (cssCheckResult !== null) return cssCheckResult;
|
|
63
|
+
// ... expensive check ...
|
|
64
|
+
cssCheckResult = hasAnimation;
|
|
65
|
+
return hasAnimation;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
45
68
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
-
|
|
68
|
-
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
|
|
76
|
-
|
|
69
|
+
**Impact:**
|
|
70
|
+
- Validation runs ONLY ONCE per page load
|
|
71
|
+
- Eliminates layout thrashing from repeated `getComputedStyle()` calls
|
|
72
|
+
- For 100 elements: **99 fewer reflows** (100 → 1)
|
|
73
|
+
- Zero memory overhead (single boolean)
|
|
74
|
+
|
|
75
|
+
### Current Performance Metrics
|
|
76
|
+
|
|
77
|
+
| Metric | Value |
|
|
78
|
+
|---------|--------|
|
|
79
|
+
| **Bundle size** | 12.4KB (compressed), 40.3KB (unpacked) |
|
|
80
|
+
| **Initialization** | ~1-2ms per element |
|
|
81
|
+
| **Observer callback** | <0.5ms per frame |
|
|
82
|
+
| **CSS validation** | ~0.1ms total (v2.2.0, with cache) |
|
|
83
|
+
| **Memory per observer** | ~1.2KB |
|
|
84
|
+
| **Animation performance** | 60fps maintained |
|
|
85
|
+
| **Memory leaks** | 0 detected |
|
|
86
|
+
|
|
87
|
+
### Why Performance Matters
|
|
88
|
+
|
|
89
|
+
**Layout Thrashing:**
|
|
90
|
+
- Synchronous reflows block the main thread
|
|
91
|
+
- Each reflow can take 10-20ms
|
|
92
|
+
- For 100 elements = **1-2 seconds blocked**
|
|
93
|
+
- User sees stuttering/jank while scrolling
|
|
94
|
+
|
|
95
|
+
**Solution:**
|
|
96
|
+
- Cache = 1 reflow instead of N
|
|
97
|
+
- 99% improvement on pages with many animations
|
|
98
|
+
- Smoother scrolling, better UX
|
|
99
|
+
|
|
100
|
+
### Optimized Code Patterns
|
|
101
|
+
|
|
102
|
+
**IntersectionObserver:**
|
|
103
|
+
- Native API (no scroll listeners)
|
|
104
|
+
- Fast callback (<0.5ms per frame)
|
|
105
|
+
- No debounce needed (browser handles this efficiently)
|
|
106
|
+
|
|
107
|
+
**CSS Animations:**
|
|
108
|
+
- Transforms only (GPU-accelerated)
|
|
109
|
+
- No layout/repaint during animation
|
|
110
|
+
- `will-change` on visible elements only
|
|
111
|
+
|
|
112
|
+
**DOM Operations:**
|
|
113
|
+
- `insertAdjacentElement('beforebegin')` instead of `insertBefore`
|
|
114
|
+
- `offsetHeight` instead of `getBoundingClientRect()` (avoids transform issues)
|
|
115
|
+
- Complete cleanup on destroy
|
|
116
|
+
|
|
117
|
+
**Memory Management:**
|
|
118
|
+
- All observers disconnected
|
|
119
|
+
- Sentinel and wrapper removed
|
|
120
|
+
- State prevents double-disconnects
|
|
121
|
+
- 0 memory leaks detected (121/121 tests)
|
|
122
|
+
|
|
123
|
+
### Future Considerations
|
|
124
|
+
|
|
125
|
+
**1. `will-change` Timing**
|
|
126
|
+
- Currently: `.is-visible { will-change: transform, opacity; }`
|
|
127
|
+
- Trade-off: Stays active after animation (consumes GPU memory)
|
|
128
|
+
- Consideration: Use `transitionend` event to remove `will-change`
|
|
129
|
+
- Recommendation: Keep current (GPU memory is cheap)
|
|
130
|
+
|
|
131
|
+
**2. Threshold Tuning**
|
|
132
|
+
- Current: `threshold: 0` (triggers as soon as 1px is visible)
|
|
133
|
+
- Alternative: `threshold: 0.1` or `threshold: 0.25`
|
|
134
|
+
- Trade-off: Higher threshold = later trigger = smoother stagger
|
|
135
|
+
- Recommendation: Keep `threshold: 0` for immediate feedback
|
|
136
|
+
|
|
137
|
+
**3. requestIdleCallback**
|
|
138
|
+
- Potential: Defer non-critical setup to browser idle time
|
|
139
|
+
- Trade-off: Complex to implement, marginal benefit
|
|
140
|
+
- Recommendation: Not needed (current performance is excellent)
|
|
141
|
+
|
|
142
|
+
**4. Testing on Low-End Devices**
|
|
143
|
+
- Test on mobile phones, older browsers
|
|
144
|
+
- Use DevTools CPU throttling
|
|
145
|
+
- Consider Lighthouse/Puppeteer for automated testing
|
|
146
|
+
- Ensure 60fps maintained on real devices
|
|
147
|
+
|
|
148
|
+
### What NOT to Optimize
|
|
149
|
+
|
|
150
|
+
**Anti-patterns to avoid:**
|
|
151
|
+
|
|
152
|
+
1. ❌ **Premature optimization**
|
|
153
|
+
- Don't optimize without measurements
|
|
154
|
+
- Profile first, optimize later
|
|
155
|
+
- "Premature optimization is the root of all evil"
|
|
156
|
+
|
|
157
|
+
2. ❌ **Over-engineering**
|
|
158
|
+
- Complex solutions for small gains
|
|
159
|
+
- Keep it simple when possible
|
|
160
|
+
- Don't sacrifice readability for micro-optimizations
|
|
161
|
+
|
|
162
|
+
3. ❌ **Breaking performance for size**
|
|
163
|
+
- Bundle size matters (12.4KB is excellent)
|
|
164
|
+
- Don't add huge dependencies for minor improvements
|
|
165
|
+
|
|
166
|
+
4. ❌ **Optimizing unused paths**
|
|
167
|
+
- Focus on hot paths (element creation, scroll, intersection)
|
|
168
|
+
- Cold paths (initialization, destroy) less critical
|
|
169
|
+
|
|
170
|
+
5. ❌ **Sacrificing maintainability**
|
|
171
|
+
- Don't sacrifice code clarity for micro-optimizations
|
|
172
|
+
- Comments should explain WHY, not just WHAT
|
|
173
|
+
- Keep code simple and understandable
|
|
174
|
+
|
|
175
|
+
### Performance Testing
|
|
176
|
+
|
|
177
|
+
**Recommended approach:**
|
|
178
|
+
1. Create a benchmark with 100-1000 animated elements
|
|
179
|
+
2. Measure: initialization, first animation, scroll performance, cleanup
|
|
180
|
+
3. Profile with DevTools Performance tab
|
|
181
|
+
4. Test on real pages (not just benchmarks)
|
|
182
|
+
5. Verify 60fps is maintained during scroll
|
|
183
|
+
|
|
184
|
+
**Tools:**
|
|
185
|
+
- Chrome DevTools Performance tab
|
|
186
|
+
- Firefox Performance Profiler
|
|
187
|
+
- Web Inspector (Safari)
|
|
188
|
+
- Lighthouse (PageSpeed, accessibility, best practices)
|
|
77
189
|
|
|
78
190
|
---
|
|
79
|
-
|
|
191
|
+
|
|
80
192
|
## 📦 Installation
|
|
81
193
|
|
|
82
194
|
```bash
|
|
@@ -91,22 +203,46 @@ yarn add rune-scroller
|
|
|
91
203
|
|
|
92
204
|
## 🚀 Quick Start
|
|
93
205
|
|
|
94
|
-
|
|
206
|
+
```svelte
|
|
207
|
+
<script>
|
|
208
|
+
import runeScroller from 'rune-scroller';
|
|
209
|
+
</script>
|
|
210
|
+
|
|
211
|
+
<!-- Simple animation -->
|
|
212
|
+
<div use:runeScroller={{ animation: 'fade-in' }}>
|
|
213
|
+
<h2>Animated Heading</h2>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<!-- With custom duration -->
|
|
217
|
+
<div use:runeScroller={{ animation: 'fade-in-up', duration: 1500 }}>
|
|
218
|
+
<div class="card">Smooth fade and slide</div>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
<!-- Repeat on every scroll -->
|
|
222
|
+
<div use:runeScroller={{ animation: 'bounce-in', repeat: true }}>
|
|
223
|
+
<button>Bounces on every scroll</button>
|
|
224
|
+
</div>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
That's it! The CSS animations are included automatically when you import rune-scroller.
|
|
228
|
+
|
|
229
|
+
### Option 2: Manual CSS Import
|
|
95
230
|
|
|
96
|
-
|
|
231
|
+
For fine-grained control, import CSS manually:
|
|
97
232
|
|
|
98
|
-
**
|
|
233
|
+
**Step 1: Import CSS in your root layout (recommended for SvelteKit):**
|
|
99
234
|
|
|
100
235
|
```svelte
|
|
101
236
|
<!-- src/routes/+layout.svelte -->
|
|
102
237
|
<script>
|
|
103
238
|
import 'rune-scroller/animations.css';
|
|
239
|
+
let { children } = $props();
|
|
104
240
|
</script>
|
|
105
241
|
|
|
106
|
-
|
|
242
|
+
{@render children()}
|
|
107
243
|
```
|
|
108
244
|
|
|
109
|
-
**
|
|
245
|
+
**Or import in each component:**
|
|
110
246
|
|
|
111
247
|
```svelte
|
|
112
248
|
<script>
|
|
@@ -115,7 +251,7 @@ yarn add rune-scroller
|
|
|
115
251
|
</script>
|
|
116
252
|
```
|
|
117
253
|
|
|
118
|
-
|
|
254
|
+
**Step 2: Use the animations**
|
|
119
255
|
|
|
120
256
|
```svelte
|
|
121
257
|
<script>
|
|
@@ -123,19 +259,8 @@ yarn add rune-scroller
|
|
|
123
259
|
// CSS already imported in layout or above
|
|
124
260
|
</script>
|
|
125
261
|
|
|
126
|
-
<!-- Simple animation -->
|
|
127
262
|
<div use:runeScroller={{ animation: 'fade-in' }}>
|
|
128
|
-
|
|
129
|
-
</div>
|
|
130
|
-
|
|
131
|
-
<!-- With custom duration -->
|
|
132
|
-
<div use:runeScroller={{ animation: 'fade-in-up', duration: 1500 }}>
|
|
133
|
-
<div class="card">Smooth fade and slide</div>
|
|
134
|
-
</div>
|
|
135
|
-
|
|
136
|
-
<!-- Repeat on every scroll -->
|
|
137
|
-
<div use:runeScroller={{ animation: 'bounce-in', repeat: true }}>
|
|
138
|
-
<button>Bounces on every scroll</button>
|
|
263
|
+
Animated content
|
|
139
264
|
</div>
|
|
140
265
|
```
|
|
141
266
|
|
|
@@ -282,30 +407,6 @@ interface RuneScrollerOptions {
|
|
|
282
407
|
|
|
283
408
|
---
|
|
284
409
|
|
|
285
|
-
## 🔧 Advanced Usage
|
|
286
|
-
|
|
287
|
-
### Using Composables
|
|
288
|
-
|
|
289
|
-
```svelte
|
|
290
|
-
<script>
|
|
291
|
-
import { useIntersectionOnce } from 'rune-scroller';
|
|
292
|
-
import 'rune-scroller/animations.css';
|
|
293
|
-
|
|
294
|
-
const intersection = useIntersectionOnce({ threshold: 0.5 });
|
|
295
|
-
</script>
|
|
296
|
-
|
|
297
|
-
<div
|
|
298
|
-
bind:this={intersection.element}
|
|
299
|
-
class="scroll-animate"
|
|
300
|
-
class:is-visible={intersection.isVisible}
|
|
301
|
-
data-animation="fade-in-up"
|
|
302
|
-
>
|
|
303
|
-
Manual control over intersection state
|
|
304
|
-
</div>
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
---
|
|
308
|
-
|
|
309
410
|
## 🎯 How It Works
|
|
310
411
|
|
|
311
412
|
Rune Scroller uses **sentinel-based triggering**:
|
|
@@ -332,15 +433,16 @@ Rune Scroller uses **sentinel-based triggering**:
|
|
|
332
433
|
|
|
333
434
|
## 🌐 SSR Compatibility
|
|
334
435
|
|
|
335
|
-
Works seamlessly with SvelteKit.
|
|
436
|
+
Works seamlessly with SvelteKit. Simply import rune-scroller in your root layout:
|
|
336
437
|
|
|
337
438
|
```svelte
|
|
338
439
|
<!-- src/routes/+layout.svelte -->
|
|
339
440
|
<script>
|
|
340
|
-
import 'rune-scroller
|
|
441
|
+
import runeScroller from 'rune-scroller';
|
|
442
|
+
let { children } = $props();
|
|
341
443
|
</script>
|
|
342
444
|
|
|
343
|
-
|
|
445
|
+
{@render children()}
|
|
344
446
|
```
|
|
345
447
|
|
|
346
448
|
Then use animations anywhere in your app:
|
|
@@ -396,7 +498,7 @@ Rune Scroller exports a **single action-based API** (no components):
|
|
|
396
498
|
### Main Export
|
|
397
499
|
|
|
398
500
|
```typescript
|
|
399
|
-
//
|
|
501
|
+
// CSS is automatically included
|
|
400
502
|
import runeScroller from 'rune-scroller';
|
|
401
503
|
|
|
402
504
|
// Named exports
|
|
@@ -444,7 +546,6 @@ interface RuneScrollerOptions {
|
|
|
444
546
|
```svelte
|
|
445
547
|
<script>
|
|
446
548
|
import runeScroller from 'rune-scroller';
|
|
447
|
-
import 'rune-scroller/animations.css';
|
|
448
549
|
|
|
449
550
|
const items = [
|
|
450
551
|
{ title: 'Feature 1', description: 'Description 1' },
|
|
@@ -485,7 +586,7 @@ interface RuneScrollerOptions {
|
|
|
485
586
|
|
|
486
587
|
- **npm Package**: [rune-scroller](https://www.npmjs.com/package/rune-scroller)
|
|
487
588
|
- **GitHub**: [lelabdev/rune-scroller](https://github.com/lelabdev/rune-scroller)
|
|
488
|
-
- **Changelog**: [CHANGELOG.md](
|
|
589
|
+
- **Changelog**: [CHANGELOG.md](https://github.com/lelabdev/rune-scroller/blob/main/lib/CHANGELOG.md)
|
|
489
590
|
|
|
490
591
|
---
|
|
491
592
|
|
package/dist/dom-utils.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export function createSentinel(element: HTMLElement, debug?: boolean, offset?: n
|
|
|
24
24
|
};
|
|
25
25
|
/**
|
|
26
26
|
* Check if CSS animations are loaded and warn if not (dev only)
|
|
27
|
+
* Uses cache to avoid expensive getComputedStyle() on every element creation
|
|
27
28
|
* @returns {boolean} True if CSS appears to be loaded
|
|
28
29
|
*/
|
|
29
30
|
export function checkAndWarnIfCSSNotLoaded(): boolean;
|
package/dist/dom-utils.js
CHANGED
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
let sentinelCounter = 0;
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Cache to check CSS only once per page load
|
|
9
|
+
* Avoids expensive getComputedStyle() calls
|
|
10
|
+
* @type {boolean | null}
|
|
11
|
+
*/
|
|
12
|
+
let cssCheckResult = null;
|
|
13
|
+
|
|
7
14
|
/**
|
|
8
15
|
* @param {HTMLElement} element
|
|
9
16
|
* @param {number} [duration]
|
|
@@ -38,7 +45,7 @@ export function createSentinel(element, debug = false, offset = 0, sentinelColor
|
|
|
38
45
|
const sentinel = document.createElement('div');
|
|
39
46
|
// Use offsetHeight instead of getBoundingClientRect for accurate dimensions
|
|
40
47
|
// getBoundingClientRect returns transformed dimensions (affected by scale, etc)
|
|
41
|
-
// offsetHeight returns
|
|
48
|
+
// offsetHeight returns actual element height independent of CSS transforms
|
|
42
49
|
const elementHeight = element.offsetHeight;
|
|
43
50
|
const sentinelTop = elementHeight + offset;
|
|
44
51
|
|
|
@@ -48,7 +55,7 @@ export function createSentinel(element, debug = false, offset = 0, sentinelColor
|
|
|
48
55
|
sentinelId = `sentinel-${sentinelCounter}`;
|
|
49
56
|
}
|
|
50
57
|
|
|
51
|
-
// Always set
|
|
58
|
+
// Always set to data-sentinel-id attribute
|
|
52
59
|
sentinel.setAttribute('data-sentinel-id', sentinelId);
|
|
53
60
|
|
|
54
61
|
if (debug) {
|
|
@@ -71,11 +78,15 @@ export function createSentinel(element, debug = false, offset = 0, sentinelColor
|
|
|
71
78
|
|
|
72
79
|
/**
|
|
73
80
|
* Check if CSS animations are loaded and warn if not (dev only)
|
|
81
|
+
* Uses cache to avoid expensive getComputedStyle() on every element creation
|
|
74
82
|
* @returns {boolean} True if CSS appears to be loaded
|
|
75
83
|
*/
|
|
76
84
|
export function checkAndWarnIfCSSNotLoaded() {
|
|
77
|
-
if (typeof document === 'undefined') return;
|
|
78
|
-
if (process.env.NODE_ENV === 'production') return;
|
|
85
|
+
if (typeof document === 'undefined') return false;
|
|
86
|
+
if (process.env.NODE_ENV === 'production') return true;
|
|
87
|
+
|
|
88
|
+
// Return cached result if already checked (avoids expensive reflows)
|
|
89
|
+
if (cssCheckResult !== null) return cssCheckResult;
|
|
79
90
|
|
|
80
91
|
// Try to detect if animations.css is loaded by checking for animation classes
|
|
81
92
|
const test = document.createElement('div');
|
|
@@ -94,4 +105,8 @@ export function checkAndWarnIfCSSNotLoaded() {
|
|
|
94
105
|
'Documentation: https://github.com/lelabdev/rune-scroller#installation'
|
|
95
106
|
);
|
|
96
107
|
}
|
|
108
|
+
|
|
109
|
+
// Cache the result for future calls
|
|
110
|
+
cssCheckResult = hasAnimation;
|
|
111
|
+
return hasAnimation;
|
|
97
112
|
}
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rune-scroller",
|
|
3
|
-
"version": "2.1
|
|
4
|
-
"description": "Lightweight, high-performance scroll animations for Svelte 5.
|
|
3
|
+
"version": "2.2.1",
|
|
4
|
+
"description": "Lightweight, high-performance scroll animations for Svelte 5. 14KB gzipped, zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"license": "MIT",
|