CSS Media Queries Analysis
Analyze all @media rules in CSS stylesheets to identify classes and properties targeting viewports bigger than a specified breakpoint (default: 768px). Results are grouped by inline CSS and external files, with byte size estimates for potential mobile savings.
This page provides two complementary analysis functions:
analyzeCSSMediaQueries(minWidth)- Identifies desktop-specific CSS and calculates byte savingsanalyzeCSSPerformanceImpact(minWidth)- Estimates real-world performance impact across device profiles, including Core Web Vitals effects, render-blocking time, and business impact projections
Snippet
// Analyze CSS @media rules for viewports bigger than a specified breakpoint
// Default minWidth = 768 (px), but you can customize it
async function analyzeCSSMediaQueries(minWidth = 768) {
const stylesheets = [...document.styleSheets];
const inlineMediaQueries = [];
const fileMediaQueries = [];
let inlineTotalClasses = 0;
let inlineTotalProperties = 0;
let filesTotalClasses = 0;
let filesTotalProperties = 0;
let inlineTotalBytes = 0;
let filesTotalBytes = 0;
let corsBlockedCount = 0;
// Helper to check if media query targets bigger than specified breakpoint
function isBiggerThanBreakpoint(mediaText) {
if (!mediaText) return false;
// Check for min-width greater than specified breakpoint
const minWidthMatch = mediaText.match(/min-width:\s*(\d+)(px|em|rem)/i);
if (minWidthMatch) {
const value = parseInt(minWidthMatch[1]);
const unit = minWidthMatch[2].toLowerCase();
if (unit === "px" && value > minWidth) return true;
if (unit === "em" && value > minWidth / 16) return true; // Convert to em
if (unit === "rem" && value > minWidth / 16) return true; // Convert to rem
}
// Check for max-width to exclude (mobile-only queries)
const maxWidthMatch = mediaText.match(/max-width:\s*(\d+)(px|em|rem)/i);
if (maxWidthMatch && !minWidthMatch) {
return false; // max-width only queries are for smaller viewports
}
return false;
}
// Helper to count classes and properties in CSS text
function countClassesAndProperties(cssText) {
const classMatches = cssText.match(/\.[a-zA-Z0-9_-]+/g) || [];
const propertyMatches = cssText.match(/[a-z-]+\s*:/g) || [];
return {
classes: classMatches.length,
properties: propertyMatches.length,
};
}
// Helper to calculate byte size
function getByteSize(text) {
return new Blob([text]).size;
}
// Helper to format bytes
function formatBytes(bytes) {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
// Parse CSS text to find @media rules
function parseMediaQueriesFromCSS(cssText, source, isInline) {
// Improved regex to capture complete @media rules
const mediaRegex = /@media\s*([^{]+)\{((?:[^{}]|\{[^{}]*\})*)\}/g;
let match;
while ((match = mediaRegex.exec(cssText)) !== null) {
const mediaText = match[1].trim();
const mediaContent = match[2];
if (isBiggerThanBreakpoint(mediaText)) {
const counts = countClassesAndProperties(mediaContent);
const byteSize = getByteSize(match[0]);
const mediaQueryData = {
source: source,
mediaQuery: mediaText,
classes: counts.classes,
properties: counts.properties,
bytes: byteSize,
bytesFormatted: formatBytes(byteSize),
};
if (isInline) {
inlineMediaQueries.push(mediaQueryData);
inlineTotalClasses += counts.classes;
inlineTotalProperties += counts.properties;
inlineTotalBytes += byteSize;
} else {
fileMediaQueries.push(mediaQueryData);
filesTotalClasses += counts.classes;
filesTotalProperties += counts.properties;
filesTotalBytes += byteSize;
}
}
}
}
// Process stylesheets
for (let sheetIndex = 0; sheetIndex < stylesheets.length; sheetIndex++) {
const sheet = stylesheets[sheetIndex];
const isInline = !sheet.href;
const source = sheet.href || `<style> tag #${sheetIndex}`;
try {
// Try to access via cssRules first
const rules = sheet.cssRules || sheet.rules;
if (rules) {
[...rules].forEach((rule) => {
if (rule.type === CSSRule.MEDIA_RULE) {
const mediaText = rule.media.mediaText;
if (isBiggerThanBreakpoint(mediaText)) {
const cssText = rule.cssText;
const counts = countClassesAndProperties(cssText);
const byteSize = getByteSize(cssText);
const mediaQueryData = {
source: source,
mediaQuery: mediaText,
classes: counts.classes,
properties: counts.properties,
bytes: byteSize,
bytesFormatted: formatBytes(byteSize),
};
if (isInline) {
inlineMediaQueries.push(mediaQueryData);
inlineTotalClasses += counts.classes;
inlineTotalProperties += counts.properties;
inlineTotalBytes += byteSize;
} else {
fileMediaQueries.push(mediaQueryData);
filesTotalClasses += counts.classes;
filesTotalProperties += counts.properties;
filesTotalBytes += byteSize;
}
}
}
});
}
} catch (e) {
// If CORS blocked, try to fetch the CSS
if (sheet.href) {
try {
const response = await fetch(sheet.href);
const cssText = await response.text();
parseMediaQueriesFromCSS(cssText, sheet.href, false);
} catch (fetchError) {
// Silently count CORS blocked files
corsBlockedCount++;
}
}
}
}
const totalBytes = inlineTotalBytes + filesTotalBytes;
// Display results
console.group(`📊 CSS Media Queries Analysis (min-width > ${minWidth}px)`);
console.log(`Total @media rules found: ${inlineMediaQueries.length + fileMediaQueries.length}`);
console.log(`Total classes: ${inlineTotalClasses + filesTotalClasses}`);
console.log(`Total properties: ${inlineTotalProperties + filesTotalProperties}`);
console.groupEnd();
// Mobile Savings Estimate
console.group("💾 POTENTIAL MOBILE SAVINGS");
console.log(
`%cEstimated CSS bytes that mobile doesn't need: ${formatBytes(totalBytes)}`,
"font-weight: bold; color: #22c55e; font-size: 14px;",
);
console.log(` └─ From inline CSS: ${formatBytes(inlineTotalBytes)}`);
console.log(` └─ From external files: ${formatBytes(filesTotalBytes)}`);
console.log("");
console.log("💡 By splitting these styles into separate files with media queries,");
console.log(" mobile users won't need to download, parse, or process this CSS.");
console.groupEnd();
// Inline CSS Results
console.group("🔷 INLINE CSS (<style> tags)");
console.log(`Media queries: ${inlineMediaQueries.length}`);
console.log(`Classes: ${inlineTotalClasses}`);
console.log(`Properties: ${inlineTotalProperties}`);
console.log(`Total size: ${formatBytes(inlineTotalBytes)}`);
if (inlineMediaQueries.length === 0) {
console.log(`No inline media queries found for viewports > ${minWidth}px.`);
}
console.groupEnd();
// External Files Results
console.group("📁 EXTERNAL FILES (.css files)");
console.log(`Media queries: ${fileMediaQueries.length}`);
console.log(`Classes: ${filesTotalClasses}`);
console.log(`Properties: ${filesTotalProperties}`);
console.log(`Total size: ${formatBytes(filesTotalBytes)}`);
if (fileMediaQueries.length === 0) {
console.log(`No external file media queries found for viewports > ${minWidth}px.`);
}
console.groupEnd();
// CORS warning if applicable
if (corsBlockedCount > 0) {
console.group("⚠️ CORS LIMITATIONS");
console.log(
`%c${corsBlockedCount} external CSS file(s) could not be analyzed due to CORS restrictions.`,
"color: #f59e0b; font-weight: bold",
);
console.log("");
console.log("These files are loaded by the browser but cannot be read via JavaScript.");
console.log("The analysis above reflects only the CSS files that were accessible.");
console.log("");
console.log("💡 To analyze CORS-blocked files:");
console.log(" • Download the CSS files manually and run the analysis locally");
console.log(" • Use Chrome DevTools Coverage tab to measure unused CSS");
console.log(" • Run this analysis from the same origin as the CSS files");
console.groupEnd();
}
return {
summary: {
total: {
mediaQueries: inlineMediaQueries.length + fileMediaQueries.length,
classes: inlineTotalClasses + filesTotalClasses,
properties: inlineTotalProperties + filesTotalProperties,
bytes: totalBytes,
bytesFormatted: formatBytes(totalBytes),
},
inline: {
mediaQueries: inlineMediaQueries.length,
classes: inlineTotalClasses,
properties: inlineTotalProperties,
bytes: inlineTotalBytes,
bytesFormatted: formatBytes(inlineTotalBytes),
},
files: {
mediaQueries: fileMediaQueries.length,
classes: filesTotalClasses,
properties: filesTotalProperties,
bytes: filesTotalBytes,
bytesFormatted: formatBytes(filesTotalBytes),
},
corsBlocked: corsBlockedCount,
},
details: {
inline: inlineMediaQueries,
files: fileMediaQueries,
},
};
}
// CSS Performance Impact Analyzer
// Estimates the real-world performance cost of unnecessary CSS on mobile devices
async function analyzeCSSPerformanceImpact(minWidth = 768) {
console.log("🔍 Analyzing CSS performance impact...\n");
// First, run the media queries analysis
const mediaQueryResults = await analyzeCSSMediaQueries(minWidth);
const unnecessaryBytes = mediaQueryResults.summary.total.bytes;
// If no data was found, show early exit message
if (unnecessaryBytes === 0 && mediaQueryResults.summary.corsBlocked > 0) {
console.log(
"%c⚠️ Unable to perform performance analysis",
"color: #f59e0b; font-weight: bold; font-size: 14px",
);
console.log("");
console.log(`All CSS files (${mediaQueryResults.summary.corsBlocked}) are blocked by CORS.`);
console.log("No desktop-specific @media queries could be analyzed.");
console.log("");
console.log("Please try one of these alternatives:");
console.log(" • Run this script from the same domain as the CSS files");
console.log(" • Use Chrome DevTools Coverage tab for manual analysis");
console.log(" • Download CSS files and analyze them locally");
return null;
}
if (unnecessaryBytes === 0) {
console.log("%c✅ Great news!", "color: #22c55e; font-weight: bold; font-size: 14px");
console.log("");
console.log("No desktop-specific CSS found (min-width > " + minWidth + "px).");
console.log("This site appears to be optimized for mobile-first delivery.");
return null;
}
// Device profiles based on real Chrome UX Report data
const deviceProfiles = {
"High-end (Pixel 7, iPhone 14)": {
cssParsingSpeed: 1.5, // MB/s
cssSelectorMatchingMultiplier: 1.0,
networkSpeed: 10, // Mbps (4G LTE)
description: "Top 25% devices",
},
"Mid-range (Moto G Power, iPhone SE)": {
cssParsingSpeed: 0.8, // MB/s
cssSelectorMatchingMultiplier: 1.8,
networkSpeed: 5, // Mbps (4G)
description: "Median mobile device",
},
"Low-end (Moto E, older devices)": {
cssParsingSpeed: 0.3, // MB/s
cssSelectorMatchingMultiplier: 3.5,
networkSpeed: 2, // Mbps (3G/slow 4G)
description: "Bottom 25% devices",
},
};
const totalClasses = mediaQueryResults.summary.total.classes;
const totalProperties = mediaQueryResults.summary.total.properties;
console.group("⚡ PERFORMANCE IMPACT ANALYSIS");
console.log(`Unnecessary CSS size: ${mediaQueryResults.summary.total.bytesFormatted}`);
console.log(`Classes to process: ${totalClasses}`);
console.log(`Properties to compute: ${totalProperties}\n`);
Object.entries(deviceProfiles).forEach(([deviceName, profile]) => {
console.group(`📱 ${deviceName}`);
console.log(` ${profile.description}`);
console.log("");
// 1. Network download time
const downloadTimeMs = (unnecessaryBytes * 8) / (profile.networkSpeed * 1000);
// 2. CSS Parsing time (converting bytes to styles)
const parsingTimeMs = (unnecessaryBytes / (1024 * 1024) / profile.cssParsingSpeed) * 1000;
// 3. CSSOM construction (creating the CSS Object Model)
// Approximation: ~0.01ms per CSS property on mid-range devices
const cssomConstructionMs = totalProperties * 0.01 * profile.cssSelectorMatchingMultiplier;
// 4. Selector matching overhead
// Each class needs to be evaluated against the DOM (even if it doesn't match)
// ~0.005ms per selector on mid-range devices
const selectorMatchingMs = totalClasses * 0.005 * profile.cssSelectorMatchingMultiplier;
// 5. Style recalculation overhead during interactions
// More CSS rules = more time in each style recalc
const recalcOverheadMs = totalProperties * 0.002 * profile.cssSelectorMatchingMultiplier;
const totalBlockingTime = downloadTimeMs + parsingTimeMs + cssomConstructionMs;
const totalRuntimeOverhead = selectorMatchingMs + recalcOverheadMs;
console.log("🚦 Render-blocking impact:");
console.log(` • Network download: ${downloadTimeMs.toFixed(2)}ms`);
console.log(` • CSS parsing: ${parsingTimeMs.toFixed(2)}ms`);
console.log(` • CSSOM construction: ${cssomConstructionMs.toFixed(2)}ms`);
console.log(
` 📊 Total blocking time: %c${totalBlockingTime.toFixed(2)}ms`,
"font-weight: bold; color: #ef4444",
);
console.log("");
console.log("⚙️ Runtime overhead (per page interaction):");
console.log(` • Selector matching: ${selectorMatchingMs.toFixed(2)}ms`);
console.log(` • Style recalculation: ${recalcOverheadMs.toFixed(2)}ms`);
console.log(
` 📊 Total per-interaction: %c${totalRuntimeOverhead.toFixed(2)}ms`,
"font-weight: bold; color: #f59e0b",
);
console.log("");
// Core Web Vitals impact estimation
console.log("📈 Core Web Vitals Impact:");
// FCP impact (part of blocking time)
const fcpImpact = totalBlockingTime * 0.6; // ~60% affects FCP
console.log(` • FCP delay: ~${fcpImpact.toFixed(0)}ms`);
// LCP impact (if there are images/text affected by these styles)
const lcpImpact = totalBlockingTime * 0.4;
console.log(` • LCP delay: ~${lcpImpact.toFixed(0)}ms`);
// INP impact (runtime overhead affects each interaction)
console.log(` • INP overhead: ~${totalRuntimeOverhead.toFixed(0)}ms per interaction`);
// TBT (Total Blocking Time) - time where main thread is blocked
const tbtContribution = Math.max(0, totalBlockingTime - 50); // Tasks >50ms contribute to TBT
console.log(` • TBT contribution: ~${tbtContribution.toFixed(0)}ms`);
console.groupEnd();
console.log("");
});
console.groupEnd();
// Savings summary
console.group("💰 POTENTIAL SAVINGS");
const midRangeProfile = deviceProfiles["Mid-range (Moto G Power, iPhone SE)"];
const midRangeDownload = (unnecessaryBytes * 8) / (midRangeProfile.networkSpeed * 1000);
const midRangeParsing =
(unnecessaryBytes / (1024 * 1024) / midRangeProfile.cssParsingSpeed) * 1000;
const midRangeCSSOM = totalProperties * 0.01 * midRangeProfile.cssSelectorMatchingMultiplier;
const midRangeTotalBlocking = midRangeDownload + midRangeParsing + midRangeCSSOM;
console.log("For the median mobile user (mid-range device):");
console.log("");
console.log(
`%c✓ Eliminate ${midRangeTotalBlocking.toFixed(0)}ms of render-blocking time`,
"font-weight: bold; color: #22c55e; font-size: 13px",
);
console.log(
`%c✓ Reduce INP by ~${(totalProperties * 0.002 * midRangeProfile.cssSelectorMatchingMultiplier).toFixed(0)}ms per interaction`,
"font-weight: bold; color: #22c55e; font-size: 13px",
);
console.log(
`%c✓ Save ${mediaQueryResults.summary.total.bytesFormatted} of bandwidth`,
"font-weight: bold; color: #22c55e; font-size: 13px",
);
console.log("");
console.log("Implementation strategy:");
console.log("1. Split desktop-specific CSS into separate file(s)");
console.log(
'2. Load with media query: <link rel="stylesheet" href="desktop.css" media="(min-width: 768px)">',
);
console.log("3. Consider critical CSS inlining for above-the-fold mobile content");
console.log("");
// Business impact estimation
const fcpImprovement = midRangeTotalBlocking * 0.6;
console.log("📊 Estimated business impact:");
console.log(` • FCP improvement: ~${fcpImprovement.toFixed(0)}ms`);
console.log(" • 100ms FCP improvement ≈ 1% conversion increase (Google/Deloitte research)");
console.log(` • Potential conversion lift: ~${(fcpImprovement / 100).toFixed(2)}%`);
console.groupEnd();
// Memory impact
console.group("🧠 MEMORY IMPACT");
// CSSOM memory estimation
// Approximately: each CSS rule = ~1KB in memory (including selector, properties, computed values)
const totalMediaRules = mediaQueryResults.summary.total.mediaQueries;
const estimatedMemoryKB = totalMediaRules * 1.0; // 1KB per rule
console.log(`Estimated CSSOM memory overhead: ~${estimatedMemoryKB.toFixed(1)} KB`);
console.log(
`Total unnecessary memory allocation: ~${(unnecessaryBytes / 1024 + estimatedMemoryKB).toFixed(1)} KB`,
);
console.log("");
console.log("💡 This memory stays allocated throughout the page lifecycle,");
console.log(" contributing to memory pressure on low-end devices.");
console.groupEnd();
// CORS disclaimer if applicable
if (mediaQueryResults.summary.corsBlocked > 0) {
console.log("");
console.log(
`%c⚠️ Note: ${mediaQueryResults.summary.corsBlocked} CSS file(s) blocked by CORS were not analyzed.`,
"color: #f59e0b; font-weight: bold",
);
console.log("The actual performance impact could be higher than shown above.");
}
// Return structured data
return {
unnecessaryBytes,
unnecessaryCss: mediaQueryResults.summary.total.bytesFormatted,
totalClasses,
totalProperties,
corsBlockedCount: mediaQueryResults.summary.corsBlocked,
deviceImpact: Object.entries(deviceProfiles).reduce((acc, [name, profile]) => {
const download = (unnecessaryBytes * 8) / (profile.networkSpeed * 1000);
const parsing = (unnecessaryBytes / (1024 * 1024) / profile.cssParsingSpeed) * 1000;
const cssom = totalProperties * 0.01 * profile.cssSelectorMatchingMultiplier;
const totalBlocking = download + parsing + cssom;
const runtime =
(totalClasses * 0.005 + totalProperties * 0.002) * profile.cssSelectorMatchingMultiplier;
acc[name] = {
renderBlockingTimeMs: totalBlocking,
runtimeOverheadMs: runtime,
fcpImpactMs: totalBlocking * 0.6,
lcpImpactMs: totalBlocking * 0.4,
inpOverheadMs: runtime,
};
return acc;
}, {}),
estimatedConversionLift: ((midRangeTotalBlocking * 0.6) / 100).toFixed(2) + "%",
};
}
// Run with default breakpoint (768px)
analyzeCSSMediaQueries();
// Or customize the breakpoint:
// analyzeCSSMediaQueries(1024); // for desktop
// analyzeCSSMediaQueries(480); // for small tabletsUnderstanding the Results
analyzeCSSMediaQueries() Results
This function provides a detailed breakdown of desktop-specific CSS:
Potential Mobile Savings:
- Estimated bytes that mobile devices don't need to download/process
- Breakdown by inline CSS vs external files
- This represents CSS that could be eliminated from mobile experience by splitting files
Inline CSS (<style> tags):
- Number of @media rules targeting viewports larger than the specified breakpoint
- Total CSS class selectors in inline media queries
- Total CSS properties in inline media queries
- Total byte size of inline media queries
External Files (.css files):
- Number of @media rules targeting viewports larger than the specified breakpoint
- Total CSS class selectors in external file media queries
- Total CSS properties in external file media queries
- Total byte size of external file media queries
Each media query includes:
- source: Stylesheet URL or inline style tag identifier
- mediaQuery: The media query condition (e.g.,
min-width: 1024px) - classes: Number of CSS class selectors
- properties: Number of CSS properties
- bytesFormatted: Size of the CSS in bytes (KB/MB)
analyzeCSSPerformanceImpact() Results
This function estimates the real-world performance cost on mobile devices:
Performance Impact Analysis (per device profile):
- High-end devices (Pixel 7, iPhone 14): Fast CSS parsing, good network
- Mid-range devices (Moto G Power, iPhone SE): Median mobile performance
- Low-end devices (Moto E, older devices): Slower parsing and network
For each profile, you'll see:
- Render-blocking impact: Network download + CSS parsing + CSSOM construction time
- Runtime overhead: Selector matching + style recalculation per interaction
- Core Web Vitals impact: FCP delay, LCP delay, INP overhead, TBT contribution
Potential Savings:
- Total render-blocking time that could be eliminated
- INP reduction per interaction
- Bandwidth savings
- Implementation strategy recommendations
Memory Impact:
- Estimated CSSOM memory overhead
- Total unnecessary memory allocation
- Impact on low-end devices
Business Impact Estimation:
- FCP improvement estimate
- Potential conversion lift based on Google/Deloitte research (100ms FCP ≈ 1% conversion increase)
Customizing the Breakpoint
By default, the snippet uses 768px as the mobile breakpoint, but you can customize it:
// Analyze media queries bigger than 1024px (desktop)
analyzeCSSMediaQueries(1024);
// Analyze media queries bigger than 480px (small tablets)
analyzeCSSMediaQueries(480);
// Analyze media queries bigger than 1440px (large desktop)
analyzeCSSMediaQueries(1440);Breakpoint Detection
The snippet considers a media query "bigger than the breakpoint" when:
min-widthis greater than the specified value (in px, em, or rem)- Excludes
max-widthonly queries (typically for smaller viewports) - Automatically converts em/rem units (assuming 16px base font size)
Solutions & Optimization Strategies
Understanding how much CSS is dedicated to larger viewports helps identify optimization opportunities:
1. Split CSS by Media Query
- Separate mobile and desktop CSS into different files
- Load desktop CSS with a media query:
<link media="(min-width: 768px)" href="desktop.css"> - Reduces initial CSS parse time on mobile devices
- Reduces style recalculation work (browser doesn't need to process desktop styles)
2. Inline Critical Above-the-Fold CSS
- Move critical mobile CSS inline in
<style>tags - Defer non-critical and desktop CSS loading
- Improves First Contentful Paint (FCP) and Largest Contentful Paint (LCP) on mobile
3. Use CSS-in-JS or CSS Modules
- Modern bundlers can split CSS by route/component
- Only load CSS when needed (code splitting)
- Reduces unused CSS on initial load
4. Consider Mobile-First Approach
- Write base styles for mobile without media queries
- Use
min-widthmedia queries for progressive enhancement - Reduces CSS overhead on mobile devices
5. Audit and Remove Unused Desktop CSS
- If desktop media queries contain excessive classes/properties
- Use tools like PurgeCSS or UnCSS to remove unused selectors
- Reduces overall CSS bundle size
6. Measure the Impact
- The "Potential Mobile Savings" metric shows exactly how many bytes mobile users could save
- Compare this against your total CSS size to understand the optimization opportunity
- Even 20-50 KB savings can improve mobile performance significantly
Further Reading
For an in-depth understanding of CSS and network performance optimization strategies, check out this excellent article:
📖 CSS and Network Performance (opens in a new tab) by Harry Roberts (opens in a new tab) (CSS Wizardry)
This article covers:
- How CSS blocks rendering and impacts performance
- Strategies for optimizing CSS delivery
- Using media queries to conditionally load CSS
- Measuring and improving CSS performance
Note
Cross-origin stylesheets cannot be accessed due to CORS restrictions. The snippet will warn about these and skip them.