Performance Optimization
Master performance optimization techniques to build fast, responsive Qlik applications that scale efficiently.
Performance Optimization Overview
Performance optimization in Qlik applications involves efficient data loading, smart caching strategies, optimized queries, and proper resource management to deliver excellent user experiences.
Data Loading
Efficient data fetching and pagination strategies
Caching
Intelligent caching of objects and data
Query Optimization
Optimized hypercube and selection strategies
Monitoring
Performance monitoring and profiling
Data Loading Optimization
Smart Pagination Strategy
Implement efficient pagination for large datasets
typescript
class SmartPagination {
private sessionObject: any;
private pageSize: number = 100;
private cache = new Map<number, any[]>();
private totalRows: number = 0;
private prefetchPages = 2; // Prefetch next 2 pages
constructor(sessionObject: any, pageSize: number = 100) {
this.sessionObject = sessionObject;
this.pageSize = pageSize;
}
async initialize(): Promise<void> {
const layout = await this.sessionObject.getLayout();
this.totalRows = layout.qHyperCube.qSize.qcy;
console.log(`Initialized pagination for ${this.totalRows} rows`);
}
async getPage(pageNumber: number): Promise<{
data: any[];
page: number;
totalPages: number;
totalRows: number;
hasNext: boolean;
hasPrevious: boolean;
}> {
// Check cache first
if (this.cache.has(pageNumber)) {
console.log(`π Page ${pageNumber} loaded from cache`);
return this.buildPageResponse(pageNumber, this.cache.get(pageNumber)!);
}
try {
// Fetch current page
const pageData = await this.fetchPageData(pageNumber);
this.cache.set(pageNumber, pageData);
// Prefetch nearby pages in background
this.prefetchNearbyPages(pageNumber);
console.log(`π Page ${pageNumber} loaded from server`);
return this.buildPageResponse(pageNumber, pageData);
} catch (error) {
console.error(`Failed to load page ${pageNumber}:`, error);
throw error;
}
}
private async fetchPageData(pageNumber: number): Promise<any[]> {
const startRow = pageNumber * this.pageSize;
const layout = await this.sessionObject.getLayout();
const hypercube = layout.qHyperCube;
const dataPage = await this.sessionObject.getHyperCubeData('/qHyperCubeDef', [{
qLeft: 0,
qTop: startRow,
qWidth: hypercube.qSize.qcx,
qHeight: Math.min(this.pageSize, this.totalRows - startRow)
}]);
return dataPage[0].qMatrix;
}
private async prefetchNearbyPages(currentPage: number): Promise<void> {
const totalPages = Math.ceil(this.totalRows / this.pageSize);
const pagesToPrefetch: number[] = [];
// Prefetch next pages
for (let i = 1; i <= this.prefetchPages; i++) {
const nextPage = currentPage + i;
if (nextPage < totalPages && !this.cache.has(nextPage)) {
pagesToPrefetch.push(nextPage);
}
}
// Prefetch previous pages
for (let i = 1; i <= this.prefetchPages; i++) {
const prevPage = currentPage - i;
if (prevPage >= 0 && !this.cache.has(prevPage)) {
pagesToPrefetch.push(prevPage);
}
}
// Fetch in background without blocking
pagesToPrefetch.forEach(pageNum => {
this.fetchPageData(pageNum)
.then(data => {
this.cache.set(pageNum, data);
console.log(`π Prefetched page ${pageNum}`);
})
.catch(error => {
console.warn(`Prefetch failed for page ${pageNum}:`, error);
});
});
}
private buildPageResponse(pageNumber: number, data: any[]): any {
const totalPages = Math.ceil(this.totalRows / this.pageSize);
return {
data,
page: pageNumber,
totalPages,
totalRows: this.totalRows,
hasNext: pageNumber < totalPages - 1,
hasPrevious: pageNumber > 0
};
}
// Cache management
clearCache(): void {
this.cache.clear();
console.log('ποΈ Pagination cache cleared');
}
getCacheStats(): any {
return {
cachedPages: this.cache.size,
totalPages: Math.ceil(this.totalRows / this.pageSize),
cacheHitRatio: this.cache.size / Math.ceil(this.totalRows / this.pageSize),
memoryUsageEstimate: this.cache.size * this.pageSize * 8 // Rough estimate
};
}
}
// Usage
async function implementSmartPagination(app: any) {
// Create large dataset hypercube
const largeDataObject = await app.createSessionObject({
qInfo: { qType: 'large-dataset' },
qHyperCubeDef: {
qDimensions: [
{ qDef: { qFieldDefs: ['Product'] } },
{ qDef: { qFieldDefs: ['Customer'] } },
{ qDef: { qFieldDefs: ['Date'] } }
],
qMeasures: [
{ qDef: { qDef: 'Sum(Sales)' } },
{ qDef: { qDef: 'Count(OrderID)' } }
],
qInitialDataFetch: [] // Don't fetch initially
}
});
const pagination = new SmartPagination(largeDataObject, 50);
await pagination.initialize();
// Load first page
const firstPage = await pagination.getPage(0);
console.log('First page:', firstPage);
// Navigation example
const nextPage = await pagination.getPage(1);
console.log('Next page loaded');
// Check cache performance
const cacheStats = pagination.getCacheStats();
console.log('Cache stats:', cacheStats);
}
Caching Strategies
Multi-Level Caching
Implement sophisticated caching for optimal performance
typescript
class MultiLevelCache {
private memoryCache = new Map<string, any>();
private sessionCache = new Map<string, any>();
private persistentCache = new Map<string, any>();
private cacheMetrics = new Map<string, any>();
constructor(
private maxMemoryItems: number = 100,
private maxSessionItems: number = 500,
private maxPersistentItems: number = 1000
) {}
// Get data with automatic cache level selection
async get(key: string, fetchFunction?: () => Promise<any>): Promise<any> {
const startTime = performance.now();
let cacheLevel = 'miss';
try {
// Level 1: Memory cache (fastest)
if (this.memoryCache.has(key)) {
cacheLevel = 'memory';
const cached = this.memoryCache.get(key);
if (this.isValid(cached)) {
this.updateMetrics(key, 'memory', performance.now() - startTime);
return cached.data;
} else {
this.memoryCache.delete(key);
}
}
// Level 2: Session cache
if (this.sessionCache.has(key)) {
cacheLevel = 'session';
const cached = this.sessionCache.get(key);
if (this.isValid(cached)) {
// Promote to memory cache
this.set(key, cached.data, 'memory', cached.ttl);
this.updateMetrics(key, 'session', performance.now() - startTime);
return cached.data;
} else {
this.sessionCache.delete(key);
}
}
// Level 3: Persistent cache (localStorage)
const persistentData = this.getPersistent(key);
if (persistentData && this.isValid(persistentData)) {
cacheLevel = 'persistent';
// Promote to higher levels
this.set(key, persistentData.data, 'session', persistentData.ttl);
this.set(key, persistentData.data, 'memory', persistentData.ttl);
this.updateMetrics(key, 'persistent', performance.now() - startTime);
return persistentData.data;
}
// Cache miss - fetch data
if (fetchFunction) {
cacheLevel = 'miss';
const data = await fetchFunction();
// Store in all levels with appropriate TTL
this.set(key, data, 'memory', 5 * 60 * 1000); // 5 minutes
this.set(key, data, 'session', 30 * 60 * 1000); // 30 minutes
this.set(key, data, 'persistent', 24 * 60 * 60 * 1000); // 24 hours
this.updateMetrics(key, 'miss', performance.now() - startTime);
return data;
}
return null;
} finally {
console.log(`π― Cache ${cacheLevel} for key '${key}' in ${(performance.now() - startTime).toFixed(2)}ms`);
}
}
// Set data in specific cache level
set(key: string, data: any, level: 'memory' | 'session' | 'persistent', ttl: number = 5 * 60 * 1000): void {
const cacheEntry = {
data,
timestamp: Date.now(),
ttl,
expires: Date.now() + ttl
};
switch (level) {
case 'memory':
this.enforceLimit(this.memoryCache, this.maxMemoryItems);
this.memoryCache.set(key, cacheEntry);
break;
case 'session':
this.enforceLimit(this.sessionCache, this.maxSessionItems);
this.sessionCache.set(key, cacheEntry);
break;
case 'persistent':
this.setPersistent(key, cacheEntry);
break;
}
}
// Intelligent cache invalidation
invalidate(pattern: string | RegExp): number {
let invalidatedCount = 0;
const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
// Invalidate across all levels
[this.memoryCache, this.sessionCache].forEach(cache => {
const keysToDelete = Array.from(cache.keys()).filter(key => regex.test(key));
keysToDelete.forEach(key => {
cache.delete(key);
invalidatedCount++;
});
});
// Invalidate persistent cache
try {
const persistentKeys = Object.keys(localStorage).filter(key =>
key.startsWith('qlik-cache-') && regex.test(key.substring(11))
);
persistentKeys.forEach(key => {
localStorage.removeItem(key);
invalidatedCount++;
});
} catch (error) {
console.warn('Failed to invalidate persistent cache:', error);
}
console.log(`ποΈ Invalidated ${invalidatedCount} cache entries matching pattern: ${pattern}`);
return invalidatedCount;
}
// Cache warming for frequently accessed data
async warmCache(warmingPlan: Array<{
key: string;
fetchFunction: () => Promise<any>;
priority: 'high' | 'normal' | 'low';
levels: ('memory' | 'session' | 'persistent')[];
}>): Promise<void> {
console.log(`π₯ Starting cache warming for ${warmingPlan.length} items`);
// Sort by priority
const sortedPlan = warmingPlan.sort((a, b) => {
const priorities = { high: 3, normal: 2, low: 1 };
return priorities[b.priority] - priorities[a.priority];
});
for (const item of sortedPlan) {
try {
const data = await item.fetchFunction();
// Store in requested levels
item.levels.forEach(level => {
const ttl = this.getTTLForLevel(level);
this.set(item.key, data, level, ttl);
});
console.log(`β
Warmed cache for '${item.key}' in levels: ${item.levels.join(', ')}`);
// Small delay for low priority items
if (item.priority === 'low') {
await new Promise(resolve => setTimeout(resolve, 10));
}
} catch (error) {
console.error(`β Cache warming failed for '${item.key}':`, error);
}
}
console.log('π― Cache warming completed');
}
// Get comprehensive cache statistics
getStatistics(): any {
const memoryStats = this.getCacheStats(this.memoryCache, 'Memory');
const sessionStats = this.getCacheStats(this.sessionCache, 'Session');
const persistentStats = this.getPersistentStats();
const hitRateStats = Array.from(this.cacheMetrics.entries()).reduce((acc, [key, metrics]) => {
acc.totalRequests += metrics.requests;
acc.totalHits += metrics.hits;
return acc;
}, { totalRequests: 0, totalHits: 0 });
return {
levels: {
memory: memoryStats,
session: sessionStats,
persistent: persistentStats
},
hitRate: hitRateStats.totalRequests > 0 ?
(hitRateStats.totalHits / hitRateStats.totalRequests) * 100 : 0,
totalRequests: hitRateStats.totalRequests,
totalHits: hitRateStats.totalHits
};
}
private isValid(cacheEntry: any): boolean {
return cacheEntry && Date.now() < cacheEntry.expires;
}
private enforceLimit(cache: Map<string, any>, maxItems: number): void {
while (cache.size >= maxItems) {
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
}
private getPersistent(key: string): any {
try {
const stored = localStorage.getItem(`qlik-cache-${key}`);
return stored ? JSON.parse(stored) : null;
} catch {
return null;
}
}
private setPersistent(key: string, cacheEntry: any): void {
try {
// Enforce persistent cache limit
const keys = Object.keys(localStorage).filter(k => k.startsWith('qlik-cache-'));
while (keys.length >= this.maxPersistentItems) {
localStorage.removeItem(keys.shift()!);
}
localStorage.setItem(`qlik-cache-${key}`, JSON.stringify(cacheEntry));
} catch (error) {
console.warn(`Failed to set persistent cache for ${key}:`, error);
}
}
private getTTLForLevel(level: string): number {
switch (level) {
case 'memory': return 5 * 60 * 1000; // 5 minutes
case 'session': return 30 * 60 * 1000; // 30 minutes
case 'persistent': return 24 * 60 * 60 * 1000; // 24 hours
default: return 5 * 60 * 1000;
}
}
private updateMetrics(key: string, level: string, time: number): void {
if (!this.cacheMetrics.has(key)) {
this.cacheMetrics.set(key, { requests: 0, hits: 0, avgTime: 0 });
}
const metrics = this.cacheMetrics.get(key);
metrics.requests++;
if (level !== 'miss') metrics.hits++;
metrics.avgTime = (metrics.avgTime + time) / 2;
}
private getCacheStats(cache: Map<string, any>, name: string): any {
const validEntries = Array.from(cache.values()).filter(entry => this.isValid(entry));
return {
name,
size: cache.size,
validEntries: validEntries.length,
expiredEntries: cache.size - validEntries.length,
utilizationPercent: (cache.size / this.getMaxForCache(name)) * 100
};
}
private getMaxForCache(name: string): number {
switch (name) {
case 'Memory': return this.maxMemoryItems;
case 'Session': return this.maxSessionItems;
default: return 100;
}
}
private getPersistentStats(): any {
try {
const keys = Object.keys(localStorage).filter(k => k.startsWith('qlik-cache-'));
return {
name: 'Persistent',
size: keys.length,
utilizationPercent: (keys.length / this.maxPersistentItems) * 100
};
} catch {
return { name: 'Persistent', size: 0, utilizationPercent: 0 };
}
}
}
// Usage
async function implementAdvancedCaching(app: any) {
const cache = new MultiLevelCache(50, 200, 500);
// Warm frequently used data
await cache.warmCache([
{
key: 'sales-summary',
fetchFunction: async () => {
const obj = await app.createSessionObject({
qHyperCubeDef: {
qMeasures: [{ qDef: { qDef: 'Sum(Sales)' } }],
qInitialDataFetch: [{ qLeft: 0, qTop: 0, qWidth: 1, qHeight: 1 }]
}
});
const layout = await obj.getLayout();
return layout.qHyperCube.qDataPages[0].qMatrix[0][0];
},
priority: 'high',
levels: ['memory', 'session', 'persistent']
}
]);
// Use cached data
const salesData = await cache.get('sales-summary');
console.log('Sales data from cache:', salesData);
// Get cache performance stats
const stats = cache.getStatistics();
console.log('Cache performance:', stats);
return cache;
}
Performance Monitoring
Performance Monitoring & Profiling
Monitor and analyze performance metrics in real-time
typescript
class PerformanceMonitor {
private metrics = new Map<string, any>();
private alerts: any[] = [];
private isMonitoring = false;
// Start comprehensive performance monitoring
startMonitoring(): void {
this.isMonitoring = true;
console.log('π Performance monitoring started');
// Monitor frame rate
this.monitorFrameRate();
// Monitor memory usage
this.monitorMemoryUsage();
// Monitor network performance
this.monitorNetworkPerformance();
}
// Monitor Qlik operation performance
async measureOperation<T>(
operationName: string,
operation: () => Promise<T>,
thresholds?: { warning: number; critical: number }
): Promise<T> {
const startTime = performance.now();
const startMemory = this.getMemoryUsage();
try {
const result = await operation();
const endTime = performance.now();
const endMemory = this.getMemoryUsage();
const metrics = {
operationName,
duration: endTime - startTime,
memoryDelta: endMemory.usedJSHeapSize - startMemory.usedJSHeapSize,
timestamp: new Date().toISOString(),
status: 'success'
};
this.recordMetrics(operationName, metrics);
// Check thresholds
if (thresholds) {
this.checkThresholds(operationName, metrics.duration, thresholds);
}
console.log(`β‘ ${operationName}: ${metrics.duration.toFixed(2)}ms, Memory: +${(metrics.memoryDelta / 1024 / 1024).toFixed(2)}MB`);
return result;
} catch (error) {
const endTime = performance.now();
const errorMetrics = {
operationName,
duration: endTime - startTime,
timestamp: new Date().toISOString(),
status: 'error',
error: error.message
};
this.recordMetrics(operationName, errorMetrics);
console.error(`β ${operationName} failed after ${(endTime - startTime).toFixed(2)}ms:`, error);
throw error;
}
}
// Get comprehensive performance report
getPerformanceReport(): any {
const report = {
summary: this.getSummaryStats(),
operations: this.getOperationStats(),
alerts: this.alerts.slice(-10), // Last 10 alerts
systemMetrics: this.getSystemMetrics(),
recommendations: this.generateRecommendations()
};
return report;
}
private recordMetrics(operationName: string, metrics: any): void {
if (!this.metrics.has(operationName)) {
this.metrics.set(operationName, {
calls: 0,
totalDuration: 0,
avgDuration: 0,
minDuration: Infinity,
maxDuration: 0,
errors: 0,
lastCall: null
});
}
const operationMetrics = this.metrics.get(operationName);
operationMetrics.calls++;
operationMetrics.lastCall = metrics.timestamp;
if (metrics.status === 'success') {
operationMetrics.totalDuration += metrics.duration;
operationMetrics.avgDuration = operationMetrics.totalDuration / operationMetrics.calls;
operationMetrics.minDuration = Math.min(operationMetrics.minDuration, metrics.duration);
operationMetrics.maxDuration = Math.max(operationMetrics.maxDuration, metrics.duration);
} else {
operationMetrics.errors++;
}
}
private checkThresholds(operationName: string, duration: number, thresholds: any): void {
if (duration > thresholds.critical) {
this.addAlert('critical', `${operationName} took ${duration.toFixed(2)}ms (critical threshold: ${thresholds.critical}ms)`);
} else if (duration > thresholds.warning) {
this.addAlert('warning', `${operationName} took ${duration.toFixed(2)}ms (warning threshold: ${thresholds.warning}ms)`);
}
}
private addAlert(level: string, message: string): void {
const alert = {
level,
message,
timestamp: new Date().toISOString()
};
this.alerts.push(alert);
console.warn(`π¨ Performance Alert [${level.toUpperCase()}]: ${message}`);
// Keep only last 50 alerts
if (this.alerts.length > 50) {
this.alerts.shift();
}
}
private monitorFrameRate(): void {
let lastFrame = performance.now();
let frameCount = 0;
let totalFrameTime = 0;
const measureFrame = (currentFrame: number) => {
if (!this.isMonitoring) return;
const deltaTime = currentFrame - lastFrame;
frameCount++;
totalFrameTime += deltaTime;
// Report every 60 frames
if (frameCount === 60) {
const avgFrameTime = totalFrameTime / frameCount;
const fps = 1000 / avgFrameTime;
if (fps < 30) {
this.addAlert('warning', `Low frame rate detected: ${fps.toFixed(1)} FPS`);
}
frameCount = 0;
totalFrameTime = 0;
}
lastFrame = currentFrame;
requestAnimationFrame(measureFrame);
};
requestAnimationFrame(measureFrame);
}
private monitorMemoryUsage(): void {
setInterval(() => {
if (!this.isMonitoring) return;
const memory = this.getMemoryUsage();
const memoryUsageMB = memory.usedJSHeapSize / 1024 / 1024;
if (memoryUsageMB > 100) { // 100MB threshold
this.addAlert('warning', `High memory usage: ${memoryUsageMB.toFixed(2)}MB`);
}
if (memoryUsageMB > 200) { // 200MB threshold
this.addAlert('critical', `Critical memory usage: ${memoryUsageMB.toFixed(2)}MB`);
}
}, 10000); // Check every 10 seconds
}
private monitorNetworkPerformance(): void {
// Monitor fetch performance
const originalFetch = window.fetch;
window.fetch = async (...args) => {
const startTime = performance.now();
try {
const response = await originalFetch(...args);
const endTime = performance.now();
const duration = endTime - startTime;
if (duration > 5000) { // 5 second threshold
this.addAlert('warning', `Slow network request: ${duration.toFixed(2)}ms to ${args[0]}`);
}
return response;
} catch (error) {
const endTime = performance.now();
this.addAlert('error', `Network request failed after ${(endTime - startTime).toFixed(2)}ms: ${error.message}`);
throw error;
}
};
}
private getMemoryUsage(): any {
if ('memory' in performance) {
return (performance as any).memory;
}
return { usedJSHeapSize: 0, totalJSHeapSize: 0 };
}
private getSummaryStats(): any {
const totalCalls = Array.from(this.metrics.values()).reduce((sum, m) => sum + m.calls, 0);
const totalErrors = Array.from(this.metrics.values()).reduce((sum, m) => sum + m.errors, 0);
return {
totalOperations: this.metrics.size,
totalCalls,
totalErrors,
errorRate: totalCalls > 0 ? (totalErrors / totalCalls) * 100 : 0,
monitoringDuration: this.isMonitoring ? 'Active' : 'Stopped'
};
}
private getOperationStats(): any[] {
return Array.from(this.metrics.entries()).map(([name, stats]) => ({
operation: name,
...stats,
successRate: stats.calls > 0 ? ((stats.calls - stats.errors) / stats.calls) * 100 : 0
}));
}
private getSystemMetrics(): any {
const memory = this.getMemoryUsage();
return {
memoryUsage: {
used: Math.round(memory.usedJSHeapSize / 1024 / 1024),
total: Math.round(memory.totalJSHeapSize / 1024 / 1024),
limit: Math.round((memory as any).jsHeapSizeLimit / 1024 / 1024)
},
connection: navigator.onLine ? 'online' : 'offline',
userAgent: navigator.userAgent
};
}
private generateRecommendations(): string[] {
const recommendations: string[] = [];
const operationStats = this.getOperationStats();
// Check for slow operations
operationStats.forEach(stat => {
if (stat.avgDuration > 2000) {
recommendations.push(`Consider optimizing '${stat.operation}' - avg duration: ${stat.avgDuration.toFixed(2)}ms`);
}
});
// Check error rates
operationStats.forEach(stat => {
if (stat.successRate < 95 && stat.calls > 10) {
recommendations.push(`High error rate for '${stat.operation}': ${(100 - stat.successRate).toFixed(1)}%`);
}
});
// Memory recommendations
const memory = this.getMemoryUsage();
const memoryUsageMB = memory.usedJSHeapSize / 1024 / 1024;
if (memoryUsageMB > 50) {
recommendations.push('Consider implementing more aggressive caching or object cleanup');
}
return recommendations.length > 0 ? recommendations : ['Performance looks good! π'];
}
stopMonitoring(): void {
this.isMonitoring = false;
console.log('π Performance monitoring stopped');
}
}
// Usage
async function implementPerformanceMonitoring(app: any) {
const monitor = new PerformanceMonitor();
monitor.startMonitoring();
// Monitor Qlik operations
const salesData = await monitor.measureOperation(
'getSalesData',
async () => {
const obj = await app.createSessionObject({
qHyperCubeDef: {
qDimensions: [{ qDef: { qFieldDefs: ['Product'] } }],
qMeasures: [{ qDef: { qDef: 'Sum(Sales)' } }],
qInitialDataFetch: [{ qLeft: 0, qTop: 0, qWidth: 2, qHeight: 100 }]
}
});
return await obj.getLayout();
},
{ warning: 1000, critical: 3000 }
);
// Get performance report
const report = monitor.getPerformanceReport();
console.log('Performance Report:', report);
return monitor;
}
π Performance Best Practices
Data Loading: Use pagination and lazy loading for large datasets
Caching: Implement multi-level caching with appropriate TTL values
Object Management: Clean up session objects when no longer needed
Query Optimization: Limit initial data fetch sizes and use efficient expressions
Memory Management: Monitor memory usage and implement cleanup strategies
Network Optimization: Batch requests and use compression when possible
UI Performance: Debounce user interactions and use virtual scrolling
Monitoring: Continuously monitor performance metrics and set up alerts
On this page
Overview
Getting Started
Examples