Improve site performance for Arabic-speaking users with faster loading
Implements lazy loading for components and optimizes Arabic font rendering using ArabicFontOptimizer and ArabicPerformanceTracker. Replit-Commit-Author: Agent Replit-Commit-Session-Id: c5f0c281-8dd8-4846-b452-4a07bcd21062 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9777c70b-fc38-4831-8d6b-78dfffe041b0/65a93bfa-c7e5-4f55-823d-9f81089502c3.jpg
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
interface ArabicPerformanceMetrics {
|
||||
rtlRenderTime: number;
|
||||
fontLoadTime: number;
|
||||
layoutShiftScore: number;
|
||||
textDirectionSwitchTime: number;
|
||||
arabicTextRenderScore: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface PerformanceThresholds {
|
||||
rtlRenderTime: number;
|
||||
fontLoadTime: number;
|
||||
layoutShiftScore: number;
|
||||
textDirectionSwitchTime: number;
|
||||
}
|
||||
|
||||
export class ArabicPerformanceTracker {
|
||||
private metrics: ArabicPerformanceMetrics[] = [];
|
||||
private thresholds: PerformanceThresholds = {
|
||||
rtlRenderTime: 50, // ms
|
||||
fontLoadTime: 200, // ms
|
||||
layoutShiftScore: 0.1,
|
||||
textDirectionSwitchTime: 100 // ms
|
||||
};
|
||||
|
||||
trackRTLPerformance(language: string): ArabicPerformanceMetrics {
|
||||
const startTime = performance.now();
|
||||
|
||||
// Measure RTL layout rendering
|
||||
const rtlContainer = document.createElement('div');
|
||||
rtlContainer.style.direction = language === 'ar' ? 'rtl' : 'ltr';
|
||||
rtlContainer.style.position = 'absolute';
|
||||
rtlContainer.style.visibility = 'hidden';
|
||||
rtlContainer.innerHTML = language === 'ar'
|
||||
? 'مرحباً بكم في منصة المساعد الذكي للمهنيين العمانيين'
|
||||
: 'Welcome to MenAssist Professional Platform';
|
||||
|
||||
document.body.appendChild(rtlContainer);
|
||||
const rtlRenderTime = performance.now() - startTime;
|
||||
|
||||
// Measure font loading for Arabic
|
||||
const fontStartTime = performance.now();
|
||||
const fontTestElement = document.createElement('span');
|
||||
fontTestElement.style.fontFamily = language === 'ar' ? 'Noto Sans Arabic' : 'Inter';
|
||||
fontTestElement.style.fontSize = '16px';
|
||||
fontTestElement.innerHTML = language === 'ar' ? 'اختبار الخط' : 'Font Test';
|
||||
rtlContainer.appendChild(fontTestElement);
|
||||
|
||||
const fontLoadTime = performance.now() - fontStartTime;
|
||||
|
||||
// Measure layout shift
|
||||
const initialWidth = rtlContainer.offsetWidth;
|
||||
const layoutShiftScore = this.measureLayoutShift(rtlContainer, initialWidth);
|
||||
|
||||
// Measure direction switch performance
|
||||
const directionSwitchTime = this.measureDirectionSwitch(rtlContainer, language);
|
||||
|
||||
document.body.removeChild(rtlContainer);
|
||||
|
||||
const metrics: ArabicPerformanceMetrics = {
|
||||
rtlRenderTime,
|
||||
fontLoadTime,
|
||||
layoutShiftScore,
|
||||
textDirectionSwitchTime: directionSwitchTime,
|
||||
arabicTextRenderScore: this.calculateTextRenderScore(rtlRenderTime, fontLoadTime),
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
this.metrics.push(metrics);
|
||||
this.analyzePerformanceIssues(metrics);
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
private measureLayoutShift(element: HTMLElement, initialWidth: number): number {
|
||||
// Simulate font loading completion
|
||||
element.style.fontDisplay = 'swap';
|
||||
const finalWidth = element.offsetWidth;
|
||||
return Math.abs(finalWidth - initialWidth) / initialWidth;
|
||||
}
|
||||
|
||||
private measureDirectionSwitch(element: HTMLElement, currentLanguage: string): number {
|
||||
const startTime = performance.now();
|
||||
|
||||
// Switch direction
|
||||
element.style.direction = currentLanguage === 'ar' ? 'ltr' : 'rtl';
|
||||
element.style.textAlign = currentLanguage === 'ar' ? 'left' : 'right';
|
||||
|
||||
// Force reflow
|
||||
element.offsetHeight;
|
||||
|
||||
return performance.now() - startTime;
|
||||
}
|
||||
|
||||
private calculateTextRenderScore(rtlTime: number, fontTime: number): number {
|
||||
const totalTime = rtlTime + fontTime;
|
||||
if (totalTime < 50) return 100;
|
||||
if (totalTime < 100) return 90;
|
||||
if (totalTime < 200) return 80;
|
||||
if (totalTime < 300) return 70;
|
||||
return 60;
|
||||
}
|
||||
|
||||
private analyzePerformanceIssues(metrics: ArabicPerformanceMetrics): void {
|
||||
const issues: string[] = [];
|
||||
|
||||
if (metrics.rtlRenderTime > this.thresholds.rtlRenderTime) {
|
||||
issues.push('RTL rendering slower than optimal');
|
||||
}
|
||||
|
||||
if (metrics.fontLoadTime > this.thresholds.fontLoadTime) {
|
||||
issues.push('Arabic font loading slower than optimal');
|
||||
}
|
||||
|
||||
if (metrics.layoutShiftScore > this.thresholds.layoutShiftScore) {
|
||||
issues.push('Significant layout shift during font loading');
|
||||
}
|
||||
|
||||
if (metrics.textDirectionSwitchTime > this.thresholds.textDirectionSwitchTime) {
|
||||
issues.push('Direction switching performance issue');
|
||||
}
|
||||
|
||||
if (issues.length > 0) {
|
||||
console.warn('Arabic Performance Issues Detected:', issues);
|
||||
this.sendPerformanceAlert(metrics, issues);
|
||||
}
|
||||
}
|
||||
|
||||
private sendPerformanceAlert(metrics: ArabicPerformanceMetrics, issues: string[]): void {
|
||||
// Send performance data to analytics endpoint
|
||||
fetch('/api/analytics/arabic-performance', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
metrics,
|
||||
issues,
|
||||
userAgent: navigator.userAgent,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
}).catch(error => {
|
||||
console.warn('Failed to send Arabic performance data:', error);
|
||||
});
|
||||
}
|
||||
|
||||
getAverageMetrics(): Partial<ArabicPerformanceMetrics> {
|
||||
if (this.metrics.length === 0) return {};
|
||||
|
||||
const totals = this.metrics.reduce((acc, metric) => ({
|
||||
rtlRenderTime: acc.rtlRenderTime + metric.rtlRenderTime,
|
||||
fontLoadTime: acc.fontLoadTime + metric.fontLoadTime,
|
||||
layoutShiftScore: acc.layoutShiftScore + metric.layoutShiftScore,
|
||||
textDirectionSwitchTime: acc.textDirectionSwitchTime + metric.textDirectionSwitchTime,
|
||||
arabicTextRenderScore: acc.arabicTextRenderScore + metric.arabicTextRenderScore
|
||||
}), {
|
||||
rtlRenderTime: 0,
|
||||
fontLoadTime: 0,
|
||||
layoutShiftScore: 0,
|
||||
textDirectionSwitchTime: 0,
|
||||
arabicTextRenderScore: 0
|
||||
});
|
||||
|
||||
const count = this.metrics.length;
|
||||
return {
|
||||
rtlRenderTime: totals.rtlRenderTime / count,
|
||||
fontLoadTime: totals.fontLoadTime / count,
|
||||
layoutShiftScore: totals.layoutShiftScore / count,
|
||||
textDirectionSwitchTime: totals.textDirectionSwitchTime / count,
|
||||
arabicTextRenderScore: totals.arabicTextRenderScore / count
|
||||
};
|
||||
}
|
||||
|
||||
getPerformanceReport(): {
|
||||
averageMetrics: Partial<ArabicPerformanceMetrics>;
|
||||
totalSamples: number;
|
||||
performanceGrade: string;
|
||||
recommendations: string[];
|
||||
} {
|
||||
const averageMetrics = this.getAverageMetrics();
|
||||
const totalSamples = this.metrics.length;
|
||||
|
||||
const performanceGrade = this.calculatePerformanceGrade(averageMetrics);
|
||||
const recommendations = this.generateRecommendations(averageMetrics);
|
||||
|
||||
return {
|
||||
averageMetrics,
|
||||
totalSamples,
|
||||
performanceGrade,
|
||||
recommendations
|
||||
};
|
||||
}
|
||||
|
||||
private calculatePerformanceGrade(metrics: Partial<ArabicPerformanceMetrics>): string {
|
||||
const score = metrics.arabicTextRenderScore || 0;
|
||||
if (score >= 90) return 'A+';
|
||||
if (score >= 80) return 'A';
|
||||
if (score >= 70) return 'B+';
|
||||
if (score >= 60) return 'B';
|
||||
return 'C';
|
||||
}
|
||||
|
||||
private generateRecommendations(metrics: Partial<ArabicPerformanceMetrics>): string[] {
|
||||
const recommendations: string[] = [];
|
||||
|
||||
if ((metrics.rtlRenderTime || 0) > this.thresholds.rtlRenderTime) {
|
||||
recommendations.push('Optimize CSS for RTL layouts using logical properties');
|
||||
recommendations.push('Implement CSS containment for Arabic text sections');
|
||||
}
|
||||
|
||||
if ((metrics.fontLoadTime || 0) > this.thresholds.fontLoadTime) {
|
||||
recommendations.push('Preload Arabic fonts with font-display: swap');
|
||||
recommendations.push('Use font subsetting for Arabic character ranges');
|
||||
}
|
||||
|
||||
if ((metrics.layoutShiftScore || 0) > this.thresholds.layoutShiftScore) {
|
||||
recommendations.push('Reserve space for Arabic text during font loading');
|
||||
recommendations.push('Use size-adjust property for Arabic font fallbacks');
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
clearMetrics(): void {
|
||||
this.metrics = [];
|
||||
}
|
||||
}
|
||||
|
||||
export const arabicPerformanceTracker = new ArabicPerformanceTracker();
|
||||
@@ -0,0 +1,121 @@
|
||||
// Arabic font optimization with CDN delivery
|
||||
export class ArabicFontOptimizer {
|
||||
private fontCache = new Map<string, boolean>();
|
||||
private loadingPromises = new Map<string, Promise<void>>();
|
||||
|
||||
async optimizeArabicFonts(language: string): Promise<void> {
|
||||
if (language !== 'ar') return;
|
||||
|
||||
const fonts = [
|
||||
{
|
||||
family: 'Noto Sans Arabic',
|
||||
weights: ['400', '500', '600', '700'],
|
||||
display: 'swap'
|
||||
}
|
||||
];
|
||||
|
||||
await Promise.all(fonts.map(font => this.loadFont(font)));
|
||||
}
|
||||
|
||||
private async loadFont(fontConfig: {
|
||||
family: string;
|
||||
weights: string[];
|
||||
display: string;
|
||||
}): Promise<void> {
|
||||
const cacheKey = `${fontConfig.family}-${fontConfig.weights.join(',')}`;
|
||||
|
||||
if (this.fontCache.has(cacheKey)) return;
|
||||
|
||||
if (this.loadingPromises.has(cacheKey)) {
|
||||
return this.loadingPromises.get(cacheKey);
|
||||
}
|
||||
|
||||
const loadPromise = this.performFontLoad(fontConfig, cacheKey);
|
||||
this.loadingPromises.set(cacheKey, loadPromise);
|
||||
|
||||
return loadPromise;
|
||||
}
|
||||
|
||||
private async performFontLoad(
|
||||
fontConfig: { family: string; weights: string[]; display: string },
|
||||
cacheKey: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Preload critical Arabic font weights
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'preload';
|
||||
link.as = 'font';
|
||||
link.type = 'font/woff2';
|
||||
link.crossOrigin = 'anonymous';
|
||||
|
||||
// Use Google Fonts CDN with optimal parameters for Arabic
|
||||
const weightParam = fontConfig.weights.join(';');
|
||||
link.href = `https://fonts.googleapis.com/css2?family=${fontConfig.family.replace(/ /g, '+')}:wght@${weightParam}&display=${fontConfig.display}&subset=arabic`;
|
||||
|
||||
document.head.appendChild(link);
|
||||
|
||||
// Create stylesheet link
|
||||
const styleLink = document.createElement('link');
|
||||
styleLink.rel = 'stylesheet';
|
||||
styleLink.href = link.href;
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
styleLink.onload = () => resolve();
|
||||
styleLink.onerror = () => reject(new Error(`Failed to load font: ${fontConfig.family}`));
|
||||
document.head.appendChild(styleLink);
|
||||
});
|
||||
|
||||
this.fontCache.set(cacheKey, true);
|
||||
this.loadingPromises.delete(cacheKey);
|
||||
} catch (error) {
|
||||
console.warn(`Font loading failed for ${fontConfig.family}:`, error);
|
||||
this.loadingPromises.delete(cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Performance monitoring for Arabic font rendering
|
||||
measureArabicRenderPerformance(): {
|
||||
renderTime: number;
|
||||
layoutShift: number;
|
||||
fontLoadTime: number;
|
||||
} {
|
||||
const startTime = performance.now();
|
||||
|
||||
// Create test element with Arabic text
|
||||
const testDiv = document.createElement('div');
|
||||
testDiv.style.position = 'absolute';
|
||||
testDiv.style.visibility = 'hidden';
|
||||
testDiv.style.fontFamily = 'Noto Sans Arabic, Arial, sans-serif';
|
||||
testDiv.style.fontSize = '16px';
|
||||
testDiv.innerHTML = 'مرحباً بكم في منصة المساعد الذكي للمهنيين العمانيين';
|
||||
|
||||
document.body.appendChild(testDiv);
|
||||
|
||||
const initialHeight = testDiv.offsetHeight;
|
||||
const renderTime = performance.now() - startTime;
|
||||
|
||||
// Measure layout shift
|
||||
setTimeout(() => {
|
||||
const finalHeight = testDiv.offsetHeight;
|
||||
const layoutShift = Math.abs(finalHeight - initialHeight) / initialHeight;
|
||||
|
||||
document.body.removeChild(testDiv);
|
||||
|
||||
return {
|
||||
renderTime,
|
||||
layoutShift,
|
||||
fontLoadTime: renderTime
|
||||
};
|
||||
}, 100);
|
||||
|
||||
document.body.removeChild(testDiv);
|
||||
|
||||
return {
|
||||
renderTime,
|
||||
layoutShift: 0,
|
||||
fontLoadTime: renderTime
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const arabicFontOptimizer = new ArabicFontOptimizer();
|
||||
Reference in New Issue
Block a user