Files
TimeTrack/static/js/mobile-performance.js
2025-07-13 10:52:20 +02:00

323 lines
11 KiB
JavaScript

// Mobile Performance Optimizations
(function() {
'use strict';
// Lazy Loading for Images
class LazyLoader {
constructor() {
this.imageObserver = null;
this.init();
}
init() {
// Check for IntersectionObserver support
if ('IntersectionObserver' in window) {
this.imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
observer.unobserve(entry.target);
}
});
}, {
rootMargin: '50px 0px',
threshold: 0.01
});
// Start observing images
this.observeImages();
} else {
// Fallback for older browsers
this.loadAllImages();
}
}
observeImages() {
const images = document.querySelectorAll('img[data-src]');
images.forEach(img => this.imageObserver.observe(img));
}
loadImage(img) {
const src = img.dataset.src;
if (!src) return;
// Create new image to preload
const newImg = new Image();
newImg.onload = () => {
img.src = src;
img.classList.add('loaded');
delete img.dataset.src;
};
newImg.src = src;
}
loadAllImages() {
const images = document.querySelectorAll('img[data-src]');
images.forEach(img => this.loadImage(img));
}
}
// Virtual Scrolling for Long Lists
class VirtualScroller {
constructor(container, options = {}) {
this.container = container;
this.itemHeight = options.itemHeight || 80;
this.bufferSize = options.bufferSize || 5;
this.items = [];
this.visibleItems = [];
if (this.container) {
this.init();
}
}
init() {
// Set up container
this.container.style.position = 'relative';
this.container.style.overflow = 'auto';
// Create spacer for scrollbar
this.spacer = document.createElement('div');
this.spacer.style.position = 'absolute';
this.spacer.style.top = '0';
this.spacer.style.left = '0';
this.spacer.style.width = '1px';
this.container.appendChild(this.spacer);
// Set up scroll listener
this.container.addEventListener('scroll', this.onScroll.bind(this));
// Initial render
this.render();
}
setItems(items) {
this.items = items;
this.spacer.style.height = `${items.length * this.itemHeight}px`;
this.render();
}
onScroll() {
cancelAnimationFrame(this.scrollFrame);
this.scrollFrame = requestAnimationFrame(() => this.render());
}
render() {
const scrollTop = this.container.scrollTop;
const containerHeight = this.container.clientHeight;
// Calculate visible range
const startIndex = Math.max(0, Math.floor(scrollTop / this.itemHeight) - this.bufferSize);
const endIndex = Math.min(
this.items.length - 1,
Math.ceil((scrollTop + containerHeight) / this.itemHeight) + this.bufferSize
);
// Update visible items
this.updateVisibleItems(startIndex, endIndex);
}
updateVisibleItems(startIndex, endIndex) {
// Implementation depends on specific use case
// This is a simplified example
console.log(`Rendering items ${startIndex} to ${endIndex}`);
}
}
// Request Idle Callback Polyfill
window.requestIdleCallback = window.requestIdleCallback || function(cb) {
const start = Date.now();
return setTimeout(function() {
cb({
didTimeout: false,
timeRemaining: function() {
return Math.max(0, 50 - (Date.now() - start));
}
});
}, 1);
};
// Debounce function for performance
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Optimize form inputs
function optimizeInputs() {
// Debounce search inputs
const searchInputs = document.querySelectorAll('input[type="search"], .search-input');
searchInputs.forEach(input => {
const originalHandler = input.oninput;
if (originalHandler) {
input.oninput = debounce(originalHandler, 300);
}
});
// Lazy load select options for large dropdowns
const largeSelects = document.querySelectorAll('select[data-lazy]');
largeSelects.forEach(select => {
select.addEventListener('focus', function loadOptions() {
// Load options on first focus
if (this.dataset.loaded) return;
const endpoint = this.dataset.lazy;
fetch(endpoint)
.then(response => response.json())
.then(options => {
options.forEach(opt => {
const option = document.createElement('option');
option.value = opt.value;
option.textContent = opt.label;
this.appendChild(option);
});
this.dataset.loaded = 'true';
});
}, { once: true });
});
}
// Reduce motion for users who prefer it
function respectReducedMotion() {
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
if (prefersReducedMotion.matches) {
document.documentElement.classList.add('reduce-motion');
}
prefersReducedMotion.addEventListener('change', (e) => {
if (e.matches) {
document.documentElement.classList.add('reduce-motion');
} else {
document.documentElement.classList.remove('reduce-motion');
}
});
}
// Optimize animations
function optimizeAnimations() {
// Use CSS containment
const cards = document.querySelectorAll('.card, .entry-card');
cards.forEach(card => {
card.style.contain = 'layout style paint';
});
// Use will-change sparingly
document.addEventListener('touchstart', (e) => {
const target = e.target.closest('.btn, .card, [data-animate]');
if (target) {
target.style.willChange = 'transform';
// Remove after animation
setTimeout(() => {
target.style.willChange = 'auto';
}, 300);
}
});
}
// Memory management
function setupMemoryManagement() {
// Clean up event listeners on page hide
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// Pause non-critical operations
if (window.pauseBackgroundOperations) {
window.pauseBackgroundOperations();
}
} else {
// Resume operations
if (window.resumeBackgroundOperations) {
window.resumeBackgroundOperations();
}
}
});
// Clean up on navigation
window.addEventListener('pagehide', () => {
// Cancel pending requests
if (window.pendingRequests) {
window.pendingRequests.forEach(request => request.abort());
}
});
}
// Battery-aware features
function setupBatteryAwareness() {
if ('getBattery' in navigator) {
navigator.getBattery().then(battery => {
function updateBatteryStatus() {
if (battery.level < 0.2 && !battery.charging) {
document.documentElement.classList.add('low-battery');
// Reduce animations and background operations
} else {
document.documentElement.classList.remove('low-battery');
}
}
battery.addEventListener('levelchange', updateBatteryStatus);
battery.addEventListener('chargingchange', updateBatteryStatus);
updateBatteryStatus();
});
}
}
// Network-aware loading
function setupNetworkAwareness() {
if ('connection' in navigator) {
const connection = navigator.connection;
function updateNetworkStatus() {
const effectiveType = connection.effectiveType;
document.documentElement.dataset.networkSpeed = effectiveType;
// Adjust quality based on connection
if (effectiveType === 'slow-2g' || effectiveType === '2g') {
document.documentElement.classList.add('low-quality');
} else {
document.documentElement.classList.remove('low-quality');
}
}
connection.addEventListener('change', updateNetworkStatus);
updateNetworkStatus();
}
}
// Initialize all optimizations
function init() {
// Only run on mobile
if (window.innerWidth > 768) return;
requestIdleCallback(() => {
new LazyLoader();
optimizeInputs();
respectReducedMotion();
optimizeAnimations();
setupMemoryManagement();
setupBatteryAwareness();
setupNetworkAwareness();
});
}
// Start when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// Export for use in other modules
window.MobilePerformance = {
LazyLoader,
VirtualScroller,
debounce
};
})();