mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 11:51:10 -05:00
10 KiB
10 KiB
Alpine.js Optimization Strategies and Best Practices
Research Summary
Comprehensive research from Alpine.js documentation focusing on performance optimization, component patterns, and best practices for the ThrillWiki frontend redesign.
Performance Optimization Strategies
1. Component Initialization and Lifecycle
Efficient Component Registration
document.addEventListener('alpine:init', () => {
Alpine.data('dropdown', () => ({
open: false,
toggle() {
this.open = !this.open
},
destroy() {
// Clean up resources to prevent memory leaks
clearInterval(this.timer);
}
}))
})
Performance Measurement
window.start = performance.now();
document.addEventListener('alpine:initialized', () => {
setTimeout(() => {
console.log(performance.now() - window.start);
});
});
2. Event Handling Optimization
Use .passive Modifier for Scroll Performance
<div @touchstart.passive="...">...</div>
<div @wheel.passive="...">...</div>
Critical: The .passive modifier prevents blocking the browser's scroll optimizations by indicating the listener won't call preventDefault().
Debounced Event Handling
<input @input.debounce.300ms="search()" placeholder="Search...">
Efficient Event Delegation
<div @click.outside="open = false" x-data="{ open: false }">
<!-- Content -->
</div>
3. Data Management Optimization
Minimize Reactive Data
Alpine.data('app', () => ({
// Only make data reactive if it needs to trigger UI updates
items: [], // Reactive - triggers UI updates
_cache: {}, // Non-reactive - use for internal state
get filteredItems() {
// Use getters for computed properties
return this.items.filter(item => item.active)
}
}))
Efficient Array Operations
// Good: Use array methods that don't trigger full re-renders
this.items.splice(index, 1); // Remove specific item
this.items.push(newItem); // Add item
// Avoid: Full array replacement when possible
// this.items = this.items.filter(...) // Triggers full re-render
4. DOM Manipulation Optimization
Use x-show vs x-if Strategically
<!-- Use x-show for frequently toggled content -->
<div x-show="isVisible" x-transition>
Frequently toggled content
</div>
<!-- Use x-if for conditionally rendered content -->
<template x-if="shouldRender">
<div>Rarely shown content</div>
</template>
Optimize x-for Loops
<!-- Always use :key for efficient list updates -->
<template x-for="item in items" :key="item.id">
<div x-text="item.name"></div>
</template>
Minimize DOM Queries
Alpine.data('component', () => ({
init() {
// Cache DOM references in init()
this.element = this.$el;
this.container = this.$refs.container;
}
}))
5. Memory Management
Proper Cleanup in destroy()
Alpine.data('timer', () => ({
timer: null,
counter: 0,
init() {
this.timer = setInterval(() => {
console.log('Increased counter to', ++this.counter);
}, 1000);
},
destroy() {
// Critical: Clean up to prevent memory leaks
clearInterval(this.timer);
}
}))
Avoid Memory Leaks in Event Listeners
Alpine.data('component', () => ({
init() {
// Use arrow functions to maintain context
this.handleResize = () => this.onResize();
window.addEventListener('resize', this.handleResize);
},
destroy() {
window.removeEventListener('resize', this.handleResize);
}
}))
Component Architecture Patterns
1. Reusable Component Registration
Global Component Registration
Alpine.data('dropdown', () => ({
open: false,
toggle() {
this.open = !this.open
},
// Encapsulate directive logic
trigger: {
['@click']() {
this.toggle()
}
},
dialogue: {
['x-show']() {
return this.open
}
}
}))
Usage in Templates
<div x-data="dropdown">
<button x-bind="trigger">Toggle</button>
<div x-bind="dialogue">Content</div>
</div>
2. State Management Patterns
Hierarchical Data Access
<div x-data="{ open: false }">
<div x-data="{ label: 'Content:' }">
<span x-text="label"></span>
<span x-show="open"></span> <!-- Accesses parent's open -->
</div>
</div>
Centralized State with $store
Alpine.store('app', {
user: null,
parks: [],
setUser(user) {
this.user = user
},
addPark(park) {
this.parks.push(park)
}
})
3. Advanced Interaction Patterns
Custom Event Dispatching
<div @park-created="handleParkCreated">
<button @click="$dispatch('park-created', { park: newPark })">
Create Park
</button>
</div>
Intersection Observer Integration
<div x-data="{ shown: false }" x-intersect="shown = true">
<div x-show="shown" x-transition>
I'm in the viewport!
</div>
</div>
Watch for Reactive Updates
<div x-data="{ search: '' }"
x-init="$watch('search', value => console.log('Search:', value))">
<input x-model="search" placeholder="Search...">
</div>
Integration with HTMX
1. Complementary Usage Patterns
HTMX for Server Communication, Alpine for Client State
<div x-data="{ loading: false, count: 0 }">
<button hx-post="/increment"
hx-target="#result"
@click="loading = true; count++"
@htmx:after-request="loading = false">
<span x-show="!loading">Increment</span>
<span x-show="loading">Loading...</span>
</button>
<div id="result" x-text="'Local count: ' + count"></div>
</div>
Coordinated State Updates
<div x-data="{ items: [] }"
@item-added.window="items.push($event.detail)">
<form hx-post="/items"
hx-target="#items-list"
@htmx:after-request="$dispatch('item-added', { item: newItem })">
<!-- Form fields -->
</form>
<div id="items-list"></div>
</div>
2. Performance Coordination
Efficient DOM Updates
<!-- Use Alpine for immediate feedback -->
<div x-data="{ optimisticUpdate: false }">
<button @click="optimisticUpdate = true"
hx-post="/action"
hx-target="#result"
@htmx:after-request="optimisticUpdate = false">
<span x-show="!optimisticUpdate">Click me</span>
<span x-show="optimisticUpdate">Processing...</span>
</button>
</div>
ThrillWiki-Specific Optimizations
1. Search Component Optimization
Alpine.data('parkSearch', () => ({
query: '',
results: [],
loading: false,
async search() {
if (!this.query.trim()) {
this.results = [];
return;
}
this.loading = true;
try {
// Use HTMX for actual search, Alpine for state
const response = await fetch(`/search/parks/?q=${encodeURIComponent(this.query)}`);
this.results = await response.json();
} catch (error) {
console.error('Search failed:', error);
this.results = [];
} finally {
this.loading = false;
}
},
get filteredResults() {
return this.results.slice(0, 10); // Limit results for performance
}
}))
2. Photo Gallery Optimization
Alpine.data('photoGallery', () => ({
photos: [],
currentIndex: 0,
loading: false,
init() {
// Lazy load images as they come into view
this.$nextTick(() => {
this.setupIntersectionObserver();
});
},
setupIntersectionObserver() {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
this.$el.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img);
});
},
destroy() {
// Clean up observer
if (this.observer) {
this.observer.disconnect();
}
}
}))
3. Form Validation Optimization
Alpine.data('parkForm', () => ({
form: {
name: '',
location: '',
operator: ''
},
errors: {},
validating: false,
async validateField(field) {
if (this.validating) return;
this.validating = true;
try {
// Use HTMX for server-side validation
const response = await fetch('/validate-park-field/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCsrfToken()
},
body: JSON.stringify({
field: field,
value: this.form[field]
})
});
const result = await response.json();
if (result.errors) {
this.errors[field] = result.errors;
} else {
delete this.errors[field];
}
} finally {
this.validating = false;
}
},
getCsrfToken() {
return document.querySelector('[name=csrfmiddlewaretoken]').value;
}
}))
Performance Monitoring
1. Component Performance Tracking
Alpine.data('performanceTracker', () => ({
init() {
const start = performance.now();
this.$nextTick(() => {
const end = performance.now();
console.log(`Component initialized in ${end - start}ms`);
});
}
}))
2. Memory Usage Monitoring
// Monitor component count and memory usage
setInterval(() => {
if (performance.memory) {
console.log('Memory usage:', {
used: Math.round(performance.memory.usedJSHeapSize / 1048576) + 'MB',
total: Math.round(performance.memory.totalJSHeapSize / 1048576) + 'MB'
});
}
}, 10000);
Implementation Priorities for ThrillWiki
High Priority
- Component Registration: Set up reusable components for common UI patterns
- Event Optimization: Use .passive modifiers for scroll events
- Memory Management: Implement proper cleanup in destroy() methods
- State Management: Optimize reactive data usage
Medium Priority
- Intersection Observer: Lazy loading for images and content
- Performance Monitoring: Track component initialization times
- Advanced Patterns: Custom event dispatching and coordination
- Search Optimization: Debounced search with result limiting
Low Priority
- Advanced State Management: Global stores for complex state
- Custom Directives: Create project-specific Alpine directives
- Performance Profiling: Detailed memory and performance analysis