CSSmediumloading
Order CSS files correctly
rule · css-order
CSS files should load before JavaScript files in the document head to ensure proper styling precedence and prevent render-blocking issues.
Code Examples
HTML Document Structure
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Page Title</title>
<!-- ✅ CSS files first -->
<link rel="stylesheet" href="reset.css">
<link rel="stylesheet" href="main.css">
<link rel="stylesheet" href="components.css">
<!-- ✅ Critical inline CSS -->
<style>
/* Critical above-the-fold styles */
.hero { display: block; }
</style>
<!-- ✅ JavaScript files after CSS -->
<script src="critical-polyfills.js"></script>
<script defer src="main.js"></script>
<!-- ✅ Async scripts can be anywhere -->
<script async src="analytics.js"></script>
</head>
<body>
<!-- Content -->
</body>
</html>Incorrect Order (Avoid)
HTML
<!-- ❌ Bad: JavaScript before CSS -->
<head>
<script src="main.js"></script> <!-- Blocks CSS parsing -->
<link rel="stylesheet" href="main.css">
</head>
<!-- ❌ Bad: Mixed order -->
<head>
<link rel="stylesheet" href="main.css">
<script src="blocking.js"></script>
<link rel="stylesheet" href="components.css"> <!-- May cause FOUC -->
</head>Why It Matters
Incorrect CSS/JS order causes render-blocking delays and FOUC—users see unstyled content while waiting for styles to load.
Framework-Specific Implementation
React with Next.js
JSX
// pages/_document.js
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html lang="en">
<Head>
{/* CSS files loaded first */}
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossOrigin="" />
<link rel="stylesheet" href="/css/globals.css" />
<link rel="stylesheet" href="/css/components.css" />
{/* Critical inline styles */}
<style jsx>{`
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
`}</style>
{/* JavaScript after CSS */}
<script
dangerouslySetInnerHTML={{
__html: `
// Critical synchronous JavaScript
if (!window.localStorage) {
document.documentElement.className += ' no-localStorage';
}
`
}}
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
// next.config.js - Optimize CSS/JS loading
module.exports = {
experimental: {
optimizeCss: true,
optimizeFonts: true
},
webpack: (config, { dev, isServer }) => {
// Ensure CSS is loaded before JS in production
if (!dev && !isServer) {
config.optimization.splitChunks.cacheGroups = {
styles: {
name: 'styles',
test: /\.(css|scss|sass)$/,
chunks: 'all',
enforce: true,
priority: 10
}
}
}
return config
}
}React with Create React App
JSX
// public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- CSS first -->
<link rel="stylesheet" href="%PUBLIC_URL%/css/normalize.css">
<link rel="stylesheet" href="%PUBLIC_URL%/css/base.css">
<!-- Critical styles inline -->
<style>
/* Above-the-fold critical CSS */
#root {
min-height: 100vh;
display: flex;
flex-direction: column;
}
</style>
<!-- JavaScript after CSS -->
<script>
// Synchronous critical JavaScript
window.APP_CONFIG = {
version: '%REACT_APP_VERSION%'
};
</script>
<title>React App</title>
</head>
<body>
<div id="root"></div>
<!-- React scripts will be injected here -->
</body>
</html>
// Custom webpack configuration (CRACO)
// craco.config.js
const { whenProd } = require('@craco/craco')
module.exports = {
webpack: {
configure: (webpackConfig) => {
whenProd(() => {
// Ensure CSS loads before JS
webpackConfig.optimization.splitChunks.cacheGroups.styles = {
name: 'styles',
test: /\.css$/,
chunks: 'all',
enforce: true,
priority: 20
}
})
return webpackConfig
}
}
}Vue.js Implementation
HTML
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<!-- CSS files first -->
<link rel="preload" href="/fonts/roboto.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/css/normalize.css">
<link rel="stylesheet" href="/css/base.css">
<!-- Critical inline CSS -->
<style>
#app {
min-height: 100vh;
font-family: Roboto, sans-serif;
}
.v-enter-active, .v-leave-active {
transition: opacity 0.3s;
}
.v-enter, .v-leave-to {
opacity: 0;
}
</style>
<!-- JavaScript after CSS -->
<script>
// Critical synchronous code
window.VUE_APP_VERSION = '<%= process.env.VUE_APP_VERSION %>';
</script>
<title>Vue App</title>
</head>
<body>
<div id="app"></div>
<!-- Vue scripts injected here -->
</body>
</html>JavaScript
// vue.config.js
module.exports = {
configureWebpack: (config) => {
if (process.env.NODE_ENV === 'production') {
// Split CSS from JS bundles
config.optimization.splitChunks.cacheGroups = {
...config.optimization.splitChunks.cacheGroups,
styles: {
name: 'styles',
test: /\.(css|vue)$/,
chunks: 'all',
enforce: true,
priority: 10
}
}
}
},
css: {
extract: {
// Ensure CSS is extracted and loaded first
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].css'
}
}
}Angular Implementation
HTML
<!-- src/index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Angular App</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSS files first -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap">
<!-- Critical inline styles -->
<style>
/* Critical CSS for initial render */
html, body {
height: 100%;
margin: 0;
font-family: Roboto, sans-serif;
}
app-root {
display: block;
min-height: 100vh;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
</style>
<!-- JavaScript after CSS -->
<script>
// Critical JavaScript
if (!window.customElements) {
document.write('<script src="polyfills-es5.js"><\/script>');
}
</script>
</head>
<body>
<app-root>
<div class="loading">Loading...</div>
</app-root>
<!-- Angular scripts will be injected here -->
</body>
</html>JavaScript
// angular.json - Build configuration
{
"projects": {
"app": {
"architect": {
"build": {
"options": {
"styles": [
"src/styles.css"
],
"scripts": [],
"extractCss": true,
"optimization": {
"styles": {
"minify": true,
"inlineCritical": true
},
"scripts": true
}
}
}
}
}
}
}Build Tool Optimization
Webpack CSS/JS Order Plugin
JavaScript
// webpack-css-first-plugin.js
class CSSFirstPlugin {
apply(compiler) {
compiler.hooks.compilation.tap('CSSFirstPlugin', (compilation) => {
compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync(
'CSSFirstPlugin',
(data, callback) => {
// Separate CSS and JS assets
const cssAssets = data.head.filter(tag =>
tag.tagName === 'link' && tag.attributes.rel === 'stylesheet'
)
const jsAssets = data.head.filter(tag =>
tag.tagName === 'script'
)
const otherAssets = data.head.filter(tag =>
!cssAssets.includes(tag) && !jsAssets.includes(tag)
)
// Reorder: other assets, then CSS, then JS
data.head = [...otherAssets, ...cssAssets, ...jsAssets]
callback(null, data)
}
)
})
}
}
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CSSFirstPlugin = require('./webpack-css-first-plugin')
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash].css',
chunkFilename: 'css/[id].[contenthash].css'
}),
new HtmlWebpackPlugin({
template: 'src/index.html',
inject: 'head',
scriptLoading: 'defer'
}),
new CSSFirstPlugin()
],
optimization: {
splitChunks: {
cacheGroups: {
styles: {
name: 'styles',
test: /\.css$/,
chunks: 'all',
enforce: true,
priority: 10
}
}
}
}
}Vite CSS Loading Optimization
JavaScript
// vite.config.js
import { defineConfig } from 'vite'
export default defineConfig({
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "src/styles/variables.scss";`
}
}
},
build: {
cssCodeSplit: true,
rollupOptions: {
output: {
// Ensure CSS loads before JS
assetFileNames: (assetInfo) => {
if (assetInfo.name.endsWith('.css')) {
return 'css/[name]-[hash][extname]'
}
return 'assets/[name]-[hash][extname]'
},
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js'
}
}
},
plugins: [
{
name: 'css-first',
generateBundle(options, bundle) {
// Ensure CSS assets are processed first
Object.keys(bundle).forEach(key => {
if (key.endsWith('.css')) {
const asset = bundle[key]
delete bundle[key]
bundle[`0-${key}`] = asset
}
})
}
}
]
})Performance Impact Analysis
Critical Path Analysis
JavaScript
// analyze-critical-path.js
const puppeteer = require('puppeteer')
async function analyzeCriticalPath(url) {
const browser = await puppeteer.launch()
const page = await browser.newPage()
// Track resource loading order
const resources = []
page.on('response', response => {
const contentType = response.headers()['content-type'] || ''
resources.push({
url: response.url(),
type: contentType.includes('text/css') ? 'CSS' :
contentType.includes('javascript') ? 'JS' : 'OTHER',
timestamp: Date.now(),
status: response.status()
})
})
await page.goto(url, { waitUntil: 'networkidle2' })
// Analyze loading order
const cssResources = resources.filter(r => r.type === 'CSS')
const jsResources = resources.filter(r => r.type === 'JS')
const firstCSS = cssResources[0]?.timestamp || Infinity
const firstJS = jsResources[0]?.timestamp || Infinity
const analysis = {
correctOrder: firstCSS < firstJS,
cssFirst: firstCSS,
jsFirst: firstJS,
totalCSS: cssResources.length,
totalJS: jsResources.length,
recommendations: []
}
if (!analysis.correctOrder) {
analysis.recommendations.push('Move CSS files before JavaScript files in document head')
}
if (cssResources.length > 5) {
analysis.recommendations.push('Consider bundling CSS files to reduce HTTP requests')
}
await browser.close()
return analysis
}
// Usage
analyzeCriticalPath('https://example.com').then(analysis => {
console.log('Critical Path Analysis:', analysis)
})FOUC Prevention
HTML
<!-- Prevent Flash of Unstyled Content -->
<head>
<!-- Critical CSS inline -->
<style>
/* Hide content until styles load */
.content {
visibility: hidden;
}
/* Show content when styles are loaded */
.styles-loaded .content {
visibility: visible;
}
</style>
<!-- External CSS -->
<link rel="stylesheet" href="main.css" onload="document.body.classList.add('styles-loaded')">
<!-- Fallback for CSS loading -->
<script>
// Show content after timeout if CSS fails to load
setTimeout(() => {
if (!document.body.classList.contains('styles-loaded')) {
document.body.classList.add('styles-loaded')
console.warn('CSS loading timeout - showing unstyled content')
}
}, 3000)
</script>
</head>
<body>
<div class="content">
<!-- Page content -->
</div>
</body>Automated Verification
CSS/JS Order Checker
JavaScript
// css-js-order-checker.js
const cheerio = require('cheerio')
const fs = require('fs')
class CSSJSOrderChecker {
checkFile(filePath) {
const content = fs.readFileSync(filePath, 'utf8')
const $ = cheerio.load(content)
const headElements = $('head').children()
const issues = []
let cssFound = false
let jsFound = false
headElements.each((index, element) => {
const tagName = element.tagName?.toLowerCase()
if (tagName === 'link') {
const rel = $(element).attr('rel')
if (rel === 'stylesheet') {
cssFound = true
if (jsFound) {
issues.push({
type: 'order',
message: 'CSS file found after JavaScript',
line: index,
element: $(element).toString()
})
}
}
}
if (tagName === 'script') {
const src = $(element).attr('src')
const async = $(element).attr('async')
const defer = $(element).attr('defer')
// Only consider blocking scripts
if (src && !async && !defer) {
jsFound = true
}
}
})
return {
file: filePath,
issues,
isValid: issues.length === 0
}
}
checkDirectory(dirPath) {
const htmlFiles = fs.readdirSync(dirPath)
.filter(file => file.endsWith('.html'))
.map(file => `${dirPath}/${file}`)
return htmlFiles.map(file => this.checkFile(file))
}
}
// Usage
const checker = new CSSJSOrderChecker()
const results = checker.checkDirectory('dist')
results.forEach(result => {
if (!result.isValid) {
console.error(`❌ ${result.file}:`)
result.issues.forEach(issue => {
console.error(` - ${issue.message}`)
})
} else {
console.log(`✅ ${result.file}: CSS/JS order correct`)
}
})ESLint Rule for Template Files
JavaScript
// eslint-plugin-css-js-order.js
module.exports = {
rules: {
'css-before-js': {
meta: {
docs: {
description: 'Enforce CSS files before JavaScript files'
},
schema: []
},
create(context) {
return {
JSXElement(node) {
if (node.openingElement.name.name === 'head') {
const children = node.children.filter(child =>
child.type === 'JSXElement'
)
let cssIndex = -1
let jsIndex = -1
children.forEach((child, index) => {
const elementName = child.openingElement.name.name
if (elementName === 'link') {
const relAttr = child.openingElement.attributes.find(attr =>
attr.name.name === 'rel'
)
if (relAttr?.value?.value === 'stylesheet') {
cssIndex = index
}
}
if (elementName === 'script') {
const srcAttr = child.openingElement.attributes.find(attr =>
attr.name.name === 'src'
)
const asyncAttr = child.openingElement.attributes.find(attr =>
attr.name.name === 'async'
)
if (srcAttr && !asyncAttr) {
jsIndex = index
}
}
})
if (cssIndex > jsIndex && jsIndex !== -1) {
context.report({
node,
message: 'CSS files should be loaded before JavaScript files'
})
}
}
}
}
}
}
}
}
// .eslintrc.js
module.exports = {
plugins: ['css-js-order'],
rules: {
'css-js-order/css-before-js': 'error'
}
}Best Practices
- CSS First: Always load CSS before JavaScript in document head
- Critical CSS: Inline above-the-fold CSS for faster rendering
- Async Scripts: Use async/defer for non-critical JavaScript
- Resource Hints: Use preload/prefetch for performance optimization
- Bundle Optimization: Configure build tools to maintain proper order
- FOUC Prevention: Implement strategies to prevent flash of unstyled content
- Monitoring: Regularly check loading order in production
- Documentation: Document exceptions and special loading requirements
Common Mistakes
❌ JavaScript before CSS
HTML
<head>
<script src="main.js"></script>
<link rel="stylesheet" href="main.css">
</head>❌ Mixed order
HTML
<head>
<link rel="stylesheet" href="base.css">
<script src="polyfills.js"></script>
<link rel="stylesheet" href="components.css">
</head>❌ No order consideration
HTML
<head>
<script async src="analytics.js"></script>
<link rel="stylesheet" href="main.css">
<script src="app.js"></script>
</head>✅ Correct implementation
HTML
<head>
<!-- CSS files first -->
<link rel="stylesheet" href="base.css">
<link rel="stylesheet" href="components.css">
<!-- JavaScript files after CSS -->
<script src="polyfills.js"></script>
<script defer src="app.js"></script>
<!-- Async scripts can be anywhere -->
<script async src="analytics.js"></script>
</head>Verification
Automated Checks
- Confirm the computed styles match the intended fix in DevTools.
- If the rule affects motion, contrast, or layout stability, verify those user-facing outcomes directly.
Manual Checks
- Inspect the rendered UI at the breakpoints and interaction states affected by the rule.
- Test at least one mobile and one desktop viewport before shipping.