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.
Snippet
// Analyze CSS @media rules for viewports bigger than a specified breakpoint
// Default minWidth = 768 (px), but you can customize it
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;
// 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];
}
// Iterate through all stylesheets
stylesheets.forEach((sheet, sheetIndex) => {
try {
const rules = sheet.cssRules || sheet.rules;
if (!rules) return;
const isInline = !sheet.href;
const source = sheet.href || `<style> tag #${sheetIndex}`;
[...rules].forEach((rule) => {
// Check if it's a media 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) {
console.warn(`Cannot access stylesheet ${sheetIndex}:`, e.message);
}
});
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.table(inlineMediaQueries, [
// "source",
// "mediaQuery",
// "classes",
// "properties",
// "bytesFormatted",
// ]);
} else {
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.table(fileMediaQueries, [
// "source",
// "mediaQuery",
// "classes",
// "properties",
// "bytesFormatted",
// ]);
} else {
console.log(`No external file media queries found for viewports > ${minWidth}px.`);
}
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),
},
},
details: {
inline: inlineMediaQueries,
files: fileMediaQueries,
},
};
}
// Run with default breakpoint (768px)
analyzeCSSMediaQueries();
// Or customize the breakpoint:
// analyzeCSSMediaQueries(1024); // for desktop
// analyzeCSSMediaQueries(480); // for small tabletsUnderstanding the Results
The snippet analyzes and groups results by:
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)
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.