Improved lighthouse performance score through inlining CSS

This commit is contained in:
2025-11-22 23:48:54 +01:00
parent 0ab715db49
commit 9572d2633a
17 changed files with 188 additions and 18 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -6,6 +6,7 @@ using Berufsschule_HAM.Services;
using Berufsschule_HAM.Models;
using Berufsschule_HAM.HealthChecks;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.ResponseCompression;
var builder = WebApplication.CreateBuilder(args);
// Bind options
@@ -50,6 +51,8 @@ builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationSc
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.Providers.Add<GzipCompressionProvider>();
options.Providers.Add<BrotliCompressionProvider>();
options.MimeTypes =
[
"text/plain",

View File

@@ -16,8 +16,8 @@
}
<form id="updateSettings" style="margin-bottom: 4rem !important" method="post" asp-controller="Settings" asp-action="Admin">
<h4 class="fw-bold">@T["General settings"]</h4>
<div class="row g-3">
<h4 class="fw-bold">@T["General settings"]</h4>
<div class="col-md-3">
<label class="form-label" for="updateHashAlgorithm">@T["Default hash algorithm"]</label>
<select type="text" name="DefaultHashAlgorithm" id="updateHashAlgorithm" class="form-control">

View File

@@ -12,14 +12,26 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Berufsschule_HAM</title>
<script type="importmap"></script>
@* <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" asp-append-version="true"/> *@
<style>
@if (Context.Request.Path.Value is not null)
{
string path = System.IO.Path.Combine("CriticalCSS", Context.Request.Path.Value.Trim('/').Replace("/", ".") + ".css");
if (File.Exists(path))
{
@Html.Raw(File.ReadAllText(path));
}
} else {
@Html.Raw(File.ReadAllText("CriticalCSS/_Layout.css"));
}
</style>
<link rel="preload" href="~/lib/bootstrap/dist/css/bootstrap.min.css" as="style"/>
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
crossorigin="anonymous">
href="~/lib/bootstrap/dist/css/bootstrap.min.css"
media="print"
onload="this.media='all'">
@* <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> *@
<link rel="preload" href="~/css/site.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="~/css/site.css"></noscript>
<link rel="stylesheet" href="~/Berufsschule_HAM.styles.css" asp-append-version="true" />
<noscript><link fetchpriority="high" rel="stylesheet" href="~/css/site.css"></noscript>
<script>
window.appTranslations = {
selectLocation: '@T["Select location"]',
@@ -38,7 +50,7 @@
</script>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar bg border-bottom box-shadow mb-3">
<a href="#main-content" class="skip-link btn btn-primary">@T["Jump to content"]</a>
<a href="#main-content" class="skip-link btn btn-primary" style="position: fixed; left: -10000px; z-index: 1000;">@T["Jump to content"]</a>
<div class="container-fluid">
<a class="" asp-area="" asp-controller="Home" asp-action="Index"><img fetchpriority="high" src="/HAM_Banner_xs.png" alt="Logo" width="123" height="40" style="width: 123px; height: 40px;"></a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"

View File

@@ -46,14 +46,3 @@ button.accept-policy {
white-space: nowrap;
line-height: 60px;
}
.skip-link {
position: fixed;
left: -10000px;
z-index: 1000;
}
.skip-link:focus {
left: 10px;
top: 10px;
outline: none;
}

145
src/critical.js Normal file
View File

@@ -0,0 +1,145 @@
import { generate } from 'critical';
import fs from 'fs';
import path from 'path';
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
// Login
await page.goto('http://localhost:5275/Home/Login');
await page.type('#username', 'admin');
await page.type('#password', 'Test1234.');
await page.click('button[type=submit]');
await page.waitForNavigation();
// Extract cookies
const cookies = await page.cookies();
await browser.close();
async function generateCriticalCSSForViews() {
const viewsDir = 'Views';
// Helper function to get all .cshtml files recursively
function getAllCshtmlFiles(dir) {
let results = [];
const list = fs.readdirSync(dir);
list.forEach(file => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat && stat.isDirectory()) {
// Recursively get files from subdirectories
results = results.concat(getAllCshtmlFiles(filePath));
} else if (file.endsWith('.cshtml')) {
results.push(filePath);
}
});
return results;
}
// Helper function to convert file path to URL path
function filePathToUrlPath(filePath) {
// Remove 'Views/' prefix
let relativePath = filePath.replace(/^Views[\/\\]/, '');
// Remove .cshtml extension
relativePath = relativePath.replace(/\.cshtml$/, '');
// Convert to URL format (replace \ with / and capitalize first letter)
const urlPath = relativePath
.split(/[\/\\]/)
.map((segment, index) =>
index === 0 ? segment : segment.charAt(0).toUpperCase() + segment.slice(1)
)
.join('/');
// Handle the case where we have a single file (like Index.cshtml)
if (relativePath.includes('/')) {
// Convert to URL path format: Views/Home/Index.cshtml -> /Home/Index
return '/' + relativePath.replace(/\\/g, '/').replace(/\.cshtml$/, '');
} else {
// For files directly in Views folder (like Views/Index.cshtml)
return '/' + relativePath.replace(/\.cshtml$/, '');
}
}
// Get all .cshtml files
const cshtmlFiles = getAllCshtmlFiles(viewsDir);
// Create CriticalCSS directory if it doesn't exist
const criticalCssDir = 'CriticalCSS';
if (!fs.existsSync(criticalCssDir)) {
fs.mkdirSync(criticalCssDir, { recursive: true });
}
// Process each file
for (const file of cshtmlFiles) {
try {
const urlPath = filePathToUrlPath(file);
// Generate critical CSS
await generate({
src: `http://localhost:5275${urlPath}`,
inline: false,
width: 1920,
height: 1080,
penthouse: {
customHeaders: {
cookie: cookies.map(c => `${c.name}=${c.value}`).join('; ')
},
forceInclude: [
'[data-bs-theme="dark"]',
'[data-bs-theme="dark"] *',
'body.dark',
'.navbar-dark',
'.navbar',
'.navbar-collapse',
'.dropdown',
'.dropdown-toggle',
'.dropdown-toggle::after',
'.dropdown-item',
'.dropdown-menu',
'.dropdown-menu-end',
'.dropdown-menu.show',
'.me-2',
'.align-items-center',
'.d-flex',
'.position-fixed', // print batch
'.bottom-0',
'.start-0',
'.m-4',
'.row', // elements
'.g-3',
'.col-md-3',
'.text-center',
'.mb-3',
'.mt-3',
'.py-4',
'.text-center',
'h2',
'.form-control',
'.btn',
'.btn-secondary',
'.modal',
]
},
target: {
css: path.join(criticalCssDir, urlPath.replace(/\//g, '.').replace(/^\./, '') + '.css')
}
});
console.log(`Critical CSS generated for: ${urlPath}`);
} catch (err) {
console.error(`Error processing ${file}:`, err);
}
}
console.log('All critical CSS files generated!');
}
// Run the function
generateCriticalCSSForViews().catch(console.error);

View File

@@ -72,3 +72,13 @@ h3.modal-title {
h4.fw-bold, h4.card-title {
font-size: 1rem;
}
.table button {
word-break: normal;
}
.skip-link:focus {
left: 10px;
top: 10px;
outline: none;
}