Performance Considerations for Search
Optimize search performance with debouncing, request cancellation, lazy loading, caching strategies, and efficient rendering.
Search performance directly impacts user experience. Slow or janky search frustrates customers and can hurt conversions. Let’s explore optimization techniques for fast, responsive search.
Debouncing Input
Prevent excessive API calls while typing:
class PredictiveSearch extends HTMLElement { constructor() { super(); this.debounceTimer = null; this.debounceDelay = 300; // milliseconds }
onInput(event) { const query = event.target.value.trim();
// Clear previous timer clearTimeout(this.debounceTimer);
// Don't search for very short queries if (query.length < 2) { this.close(); return; }
// Wait for user to stop typing this.debounceTimer = setTimeout(() => { this.search(query); }, this.debounceDelay); }}Why 300ms? It’s a balance between responsiveness and efficiency. Too short creates many requests; too long feels sluggish.
Cancelling Previous Requests
Avoid race conditions with AbortController:
class PredictiveSearch extends HTMLElement { constructor() { super(); this.abortController = null; }
async search(query) { // Cancel any in-flight request if (this.abortController) { this.abortController.abort(); }
// Create new controller for this request this.abortController = new AbortController();
try { const response = await fetch( `/search/suggest?q=${encodeURIComponent(query)}§ion_id=predictive-search`, { signal: this.abortController.signal } );
if (!response.ok) throw new Error('Search failed');
const html = await response.text(); this.renderResults(html);
} catch (error) { // Ignore abort errors (they're intentional) if (error.name === 'AbortError') return;
console.error('Search error:', error); } }}Minimum Query Length
Skip searches for very short queries:
const MIN_QUERY_LENGTH = 2;
onInput(event) { const query = event.target.value.trim();
if (query.length < MIN_QUERY_LENGTH) { this.close(); return; }
// Proceed with search}Limiting Results
Request only what you need:
/search/suggest?q=shirt&resources[limit]=4&resources[type]=productFewer results means:
- Smaller response size
- Faster rendering
- Less DOM manipulation
Lazy Loading Images
Don’t block search results for images:
<img src="{{ product.featured_image | image_url: width: 100 }}" alt="{{ product.title }}" loading="lazy" decoding="async" width="50" height="50">Or use a placeholder pattern:
<div class="search-result__image" data-src="{{ product.featured_image | image_url: width: 100 }}"> <div class="search-result__placeholder"></div></div>// Lazy load images when they enter viewportconst observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = document.createElement('img'); img.src = entry.target.dataset.src; img.onload = () => entry.target.appendChild(img); observer.unobserve(entry.target); } });});
document.querySelectorAll('[data-src]').forEach(el => observer.observe(el));Caching Results
Cache recent searches to avoid duplicate requests:
class PredictiveSearch extends HTMLElement { constructor() { super(); this.cache = new Map(); this.cacheMaxSize = 20; this.cacheTTL = 60000; // 1 minute }
async search(query) { const normalizedQuery = query.toLowerCase().trim();
// Check cache first const cached = this.cache.get(normalizedQuery); if (cached && Date.now() - cached.timestamp < this.cacheTTL) { this.renderResults(cached.html); return; }
// Fetch fresh results const html = await this.fetchResults(query);
// Store in cache this.cacheResult(normalizedQuery, html);
this.renderResults(html); }
cacheResult(query, html) { // Limit cache size if (this.cache.size >= this.cacheMaxSize) { const oldestKey = this.cache.keys().next().value; this.cache.delete(oldestKey); }
this.cache.set(query, { html, timestamp: Date.now() }); }}Optimizing Render Performance
Use DocumentFragment
Batch DOM updates:
renderResults(html) { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const fragment = document.createDocumentFragment();
doc.querySelectorAll('.search-result').forEach(result => { fragment.appendChild(result.cloneNode(true)); });
// Single DOM update this.resultsContainer.innerHTML = ''; this.resultsContainer.appendChild(fragment);}Avoid Layout Thrashing
Batch reads and writes:
// Bad: Causes layout thrashingitems.forEach(item => { const height = item.offsetHeight; // Read item.style.height = height + 'px'; // Write});
// Good: Batch reads, then batch writesconst heights = items.map(item => item.offsetHeight); // All readsitems.forEach((item, i) => { item.style.height = heights[i] + 'px'; // All writes});Preloading Search
Load the search section on hover:
let searchPreloaded = false;
document.querySelector('[data-search-toggle]').addEventListener('mouseenter', () => { if (searchPreloaded) return;
// Preload the predictive search section const link = document.createElement('link'); link.rel = 'prefetch'; link.href = '/?section_id=predictive-search'; document.head.appendChild(link);
searchPreloaded = true;}, { once: true });Skeleton Loading
Show placeholder content immediately:
<div class="predictive-search__loading"> {%- for i in (1..4) -%} <div class="search-skeleton"> <div class="search-skeleton__image"></div> <div class="search-skeleton__content"> <div class="search-skeleton__title"></div> <div class="search-skeleton__price"></div> </div> </div> {%- endfor -%}</div>.search-skeleton { display: flex; gap: var(--spacing-sm); padding: var(--spacing-sm);}
.search-skeleton__image { width: 50px; height: 50px; background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; border-radius: var(--border-radius);}
.search-skeleton__title { width: 150px; height: 16px; background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; border-radius: 4px;}
.search-skeleton__price { width: 80px; height: 14px; margin-top: 4px; background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; border-radius: 4px;}
@keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; }}Network-Aware Optimization
Adjust behavior based on connection:
class PredictiveSearch extends HTMLElement { constructor() { super(); this.debounceDelay = this.getDebounceDelay(); }
getDebounceDelay() { const connection = navigator.connection;
if (!connection) return 300;
// Slower connections get longer debounce if (connection.effectiveType === '2g') return 500; if (connection.effectiveType === '3g') return 400; if (connection.saveData) return 500;
return 300; }}Measuring Performance
Track search performance:
async search(query) { const startTime = performance.now();
try { const response = await this.fetchResults(query);
const endTime = performance.now(); const duration = endTime - startTime;
// Log slow searches if (duration > 1000) { console.warn(`Slow search: ${query} took ${duration}ms`); }
// Optional: Send to analytics if (window.gtag) { gtag('event', 'search_timing', { search_term: query, value: Math.round(duration) }); }
this.renderResults(response);
} catch (error) { // Handle error }}Complete Optimized Search
class PredictiveSearch extends HTMLElement { constructor() { super(); this.input = this.querySelector('input[type="search"]'); this.resultsContainer = this.querySelector('[data-results]'); this.loadingContainer = this.querySelector('[data-loading]');
this.debounceTimer = null; this.debounceDelay = 300; this.abortController = null; this.cache = new Map(); this.minQueryLength = 2; }
connectedCallback() { this.input.addEventListener('input', this.onInput.bind(this)); this.input.addEventListener('focus', this.onFocus.bind(this)); }
onInput(event) { const query = event.target.value.trim();
clearTimeout(this.debounceTimer);
if (query.length < this.minQueryLength) { this.close(); return; }
// Show loading immediately this.showLoading();
this.debounceTimer = setTimeout(() => { this.search(query); }, this.debounceDelay); }
async search(query) { const normalizedQuery = query.toLowerCase();
// Check cache const cached = this.cache.get(normalizedQuery); if (cached && Date.now() - cached.time < 60000) { this.renderResults(cached.html); return; }
// Cancel previous request this.abortController?.abort(); this.abortController = new AbortController();
try { const response = await fetch( `/search/suggest?q=${encodeURIComponent(query)}§ion_id=predictive-search&resources[limit]=4`, { signal: this.abortController.signal } );
if (!response.ok) throw new Error('Failed');
const html = await response.text();
// Cache result this.cache.set(normalizedQuery, { html, time: Date.now() });
// Limit cache size if (this.cache.size > 20) { const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); }
this.renderResults(html);
} catch (error) { if (error.name !== 'AbortError') { this.showError(); } } }
showLoading() { this.loadingContainer.hidden = false; this.resultsContainer.hidden = true; }
renderResults(html) { this.loadingContainer.hidden = true; this.resultsContainer.innerHTML = html; this.resultsContainer.hidden = false; }
showError() { this.loadingContainer.hidden = true; this.resultsContainer.innerHTML = '<p>Something went wrong</p>'; this.resultsContainer.hidden = false; }
close() { this.loadingContainer.hidden = true; this.resultsContainer.hidden = true; }}
customElements.define('predictive-search', PredictiveSearch);Practice Exercise
Optimize a predictive search to:
- Debounce input (300ms)
- Cancel previous requests
- Cache results for 1 minute
- Show skeleton loading
- Handle slow connections
Measure improvements with DevTools Performance tab.
Key Takeaways
- Debounce input to reduce API calls
- Cancel requests with AbortController
- Cache results for repeated queries
- Lazy load images to not block rendering
- Use skeleton loaders for perceived speed
- Limit results to what you need
- Measure performance and optimize bottlenecks
- Consider network conditions for delays
Module Complete!
Congratulations on completing Module 12! You’ve mastered:
- Search template fundamentals
- Predictive search concepts and UI
- Rendering search results
- No-results UX handling
- Search performance optimization
Next up: Module 13: Content Pages and 404 for building static pages and error handling.
Finished this lesson?
Mark it complete to track your progress.
Discussion
Loading comments...