Kurs HTML i CSS · Moduł 9: Nowoczesny CSS
Testowanie cross-browser
W tej lekcji7
Twoja strona wygląda idealnie w Chrome na laptopie, a na iPhonie klienta siatka się rozpada, bo każda przeglądarka to inny zespół kamieniarzy czytający ten sam plan po swojemu. Testowanie cross-browser to proces, który zapewnia, że aplikacje webowe działają poprawnie we wszystkich obsługiwanych przeglądarkach i na różnych urządzeniach. Różnice w implementacji standardów mogą prowadzić do niespójności w wyglądzie i funkcjonalności.
Podstawy testowania cross-browser
Główne przeglądarki do testowania
Współczesne przeglądarki opierają się na trzech silnikach: Blink (Chrome, Edge, Opera, Samsung Internet), WebKit (Safari) i Gecko (Firefox):
// Statystyki użycia przeglądarek (2024)
const browserStats = {
chrome: "65%", // Blink engine
safari: "19%", // WebKit engine
edge: "5%", // Blink engine (Chromium-based)
firefox: "3%", // Gecko engine
opera: "2%", // Blink engine
others: "6%"
};
// Mobilne przeglądarki
const mobileBrowsers = {
chrome: "65%",
safari: "25%",
samsung: "4%",
opera: "2%",
firefox: "1%",
others: "3%"
};Liczby w przykładzie są orientacyjne, a aktualne udziały znajdziesz w serwisach takich jak StatCounter. Na iPhonie przeglądarki przez lata musiały korzystać z WebKit, więc testując silnik Safari, sprawdzasz w dużej mierze także Chrome na iOS.
Kluczowe różnice między przeglądarkami
1. Wsparcie CSS
Nowe właściwości trafiały kiedyś do przeglądarek z prefiksami producentów. Wersje z prefiksami piszemy najpierw, a standardową na końcu:
/* Prefiks vendorów dla nowszych funkcji */
.element {
-webkit-transform: rotate(45deg); /* Safari, Chrome */
-moz-transform: rotate(45deg); /* Firefox */
-ms-transform: rotate(45deg); /* Internet Explorer */
transform: rotate(45deg); /* Standard */
}
/* Różnice w implementacji Grid */
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px; /* IE nie wspiera gap w Grid */
}
/* Fallback dla IE */
@supports not (gap: 20px) {
.grid-container > * {
margin: 10px;
}
}Kolejność ma znaczenie: przeglądarka, która zna standard, użyje ostatniej deklaracji. Dziś transform nie potrzebuje prefiksów, a potrzebne prefiksy dopisze Autoprefixer.
2. JavaScript API differences
W JavaScripcie przed użyciem nowego API sprawdzamy, czy ono istnieje (feature detection):
// Fetch API - nie wspierane w IE
function fetchDataCompatible(url) {
if (window.fetch) {
// Nowoczesne przeglądarki
return fetch(url).then(response => response.json());
} else {
// Fallback dla starszych przeglądarek
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error('Request failed'));
}
};
xhr.onerror = () => reject(new Error('Network error'));
xhr.send();
});
}
}
// Intersection Observer - wymaga polyfill w IE
function observeElementsCompatible(elements, callback) {
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver(callback);
elements.forEach(el => observer.observe(el));
} else {
// Fallback - scroll listener
function checkVisibility() {
elements.forEach(element => {
const rect = element.getBoundingClientRect();
const isVisible = rect.top < window.innerHeight && rect.bottom > 0;
if (isVisible) callback([{ target: element, isIntersecting: true }]);
});
}
window.addEventListener('scroll', checkVisibility);
window.addEventListener('resize', checkVisibility);
checkVisibility();
}
}Gdy fetch lub IntersectionObserver nie istnieje, kod przechodzi na starsze rozwiązanie. Sprawdzamy funkcję, a nie nazwę przeglądarki.
Narzędzia do testowania cross-browser
1. BrowserStack
Chmurowe farmy urządzeń udostępniają prawdziwe przeglądarki i systemy do testów automatycznych:
// Konfiguracja BrowserStack dla automatycznych testów
const browserStackConfig = {
user: process.env.BROWSERSTACK_USERNAME,
key: process.env.BROWSERSTACK_ACCESS_KEY,
capabilities: [
{
browserName: 'Chrome',
browserVersion: 'latest',
os: 'Windows',
osVersion: '10'
},
{
browserName: 'Safari',
browserVersion: 'latest',
os: 'OS X',
osVersion: 'Big Sur'
},
{
browserName: 'Firefox',
browserVersion: 'latest',
os: 'Windows',
osVersion: '10'
}
]
};Dane logowania pochodzą ze zmiennych środowiskowych, nigdy z kodu.
2. Sauce Labs
Sauce Labs działa podobnie, a konfiguracja w YAML wskazuje zestawy testów Cypress i przeglądarki:
# .sauce/config.yml
apiVersion: v1alpha
kind: cypress
sauce:
region: eu-central-1
suites:
- name: "Cross Browser Tests"
browser: "chrome"
browserVersion: "latest"
platformName: "Windows 10"
- name: "Safari Tests"
browser: "safari"
browserVersion: "latest"
platformName: "macOS 11.00"
cypress:
configFile: "cypress.config.js"
version: "12.0.0"Każdy zestaw ruszy w chmurze na swojej przeglądarce.
3. Playwright
Playwright uruchamia jeden zestaw testów w trzech silnikach: Chromium, Firefox i WebKit, a do tego emuluje telefony:
// playwright.config.js - konfiguracja do testowania cross-browser
const { defineConfig, devices } = require('@playwright/test');
module.exports = defineConfig({
testDir: './tests',
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
],
});
// Przykład testu cross-browser
// tests/cross-browser.spec.js
const { test, expect } = require('@playwright/test');
test.describe('Cross-browser compatibility', () => {
test('CSS Grid layout works correctly', async ({ page }) => {
await page.goto('/grid-layout');
// Sprawdź czy grid jest poprawnie wyświetlany
const gridContainer = page.locator('.grid-container');
await expect(gridContainer).toBeVisible();
// Sprawdź liczbę kolumn
const gridItems = page.locator('.grid-item');
const count = await gridItems.count();
expect(count).toBeGreaterThan(0);
// Sprawdź responsywność
await page.setViewportSize({ width: 768, height: 1024 });
await expect(gridContainer).toBeVisible();
});
test('JavaScript features work across browsers', async ({ page }) => {
await page.goto('/js-features');
// Test Fetch API lub XHR fallback
const result = await page.evaluate(async () => {
const response = await fetchDataCompatible('/api/test');
return response.status === 'ok';
});
expect(result).toBe(true);
});
});Projekty nazywają się chromium, firefox i webkit, a polecenie npx playwright test --project=webkit uruchomi tylko jeden z nich. Na start polecam Ci właśnie Playwrighta bo działa lokalnie i za darmo.
4. Lokalne środowiska testowe
Własną farmę postawisz w Dockerze. Selenium Grid składa się z huba i węzłów z przeglądarkami:
# Selenium Grid setup dla lokalnego testowania
# docker-compose.yml
version: '3'
services:
selenium-hub:
image: selenium/hub:4.0.0
container_name: selenium-hub
ports:
- "4444:4444"
chrome:
image: selenium/node-chrome:4.0.0
shm_size: 2gb
depends_on:
- selenium-hub
environment:
- HUB_HOST=selenium-hub
- HUB_PORT=4444
firefox:
image: selenium/node-firefox:4.0.0
shm_size: 2gb
depends_on:
- selenium-hub
environment:
- HUB_HOST=selenium-hub
- HUB_PORT=4444Hub rozdziela testy między węzły Chrome i Firefox. Safari w tych obrazach nie ma, do niego potrzebujesz Maca lub chmury.
Strategie kompatybilności
1. Progressive Enhancement
Progressive enhancement polega na tym, że zaczynamy od działającej bazy dla wszystkich, a ulepszenia dodajemy przeglądarkom, które je obsługują:
/* Podstawowe style dla wszystkich przeglądarek */
.card {
border: 1px solid #ddd;
padding: 16px;
margin-bottom: 16px;
background: white;
}
/* Ulepszenia dla nowoczesnych przeglądarek */
@supports (display: grid) {
.card-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.card {
margin-bottom: 0;
}
}
/* Dalsze ulepszenia */
@supports (backdrop-filter: blur(10px)) {
.card--glass {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
}
}Każdy dostaje czytelne karty, przeglądarki z Gridem dostają siatkę, a te z backdrop-filter efekt szkła. Tę samą strategię stosujesz w JavaScripcie:
// JavaScript Progressive Enhancement
class FeatureDetector {
static supportsIntersectionObserver() {
return 'IntersectionObserver' in window;
}
static supportsWebP() {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
}
static supportsCustomProperties() {
return CSS.supports && CSS.supports('(--foo: red)');
}
static init() {
// Lazy loading z fallbackiem
if (this.supportsIntersectionObserver()) {
this.initLazyLoadingModern();
} else {
this.initLazyLoadingFallback();
}
// WebP support detection
if (this.supportsWebP()) {
document.documentElement.classList.add('webp');
} else {
document.documentElement.classList.add('no-webp');
}
}
static initLazyLoadingModern() {
const images = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
imageObserver.unobserve(img);
}
});
});
images.forEach(img => imageObserver.observe(img));
}
static initLazyLoadingFallback() {
const images = document.querySelectorAll('img[data-src]');
function loadImagesInViewport() {
images.forEach(img => {
if (img.dataset.src) {
const rect = img.getBoundingClientRect();
if (rect.top < window.innerHeight + 100) {
img.src = img.dataset.src;
delete img.dataset.src;
img.classList.remove('lazy');
}
}
});
}
window.addEventListener('scroll', loadImagesInViewport);
window.addEventListener('resize', loadImagesInViewport);
loadImagesInViewport();
}
}
// Inicjalizacja przy ładowaniu strony
document.addEventListener('DOMContentLoaded', () => {
FeatureDetector.init();
});Klasy webp i no-webp na elemencie html pozwalają dobrać obrazy w CSS. Do leniwego ładowania obrazów wystarczy dziś atrybut loading="lazy".
2. Graceful Degradation
Graceful degradation idzie w drugą stronę: projektujesz dla nowoczesnych przeglądarek i pilnujesz, by starsze wciąż dawały akceptowalny wynik:
/* Nowoczesne style z fallbackami */
.hero-section {
/* Fallback dla starych przeglądarek */
background: linear-gradient(to right, #667eea, #764ba2);
/* Nowoczesne style */
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
/* Fallback dla flexbox */
.hero-section {
text-align: center;
padding-top: 20vh;
}
@supports (display: flex) {
.hero-section {
padding-top: 0;
text-align: left;
}
}
/* CSS Variables z fallbackiem */
.theme-primary {
color: #007bff; /* Fallback */
color: var(--primary-color, #007bff);
}Działa tu prosta zasada CSS: przeglądarka, która nie zrozumie drugiej deklaracji, zostanie przy pierwszej. Dlatego zwykły kolor stoi przed wersją z var().
3. Polyfills i shims
Polyfill to kod, który dostarcza brakującą funkcjonalność w starszych przeglądarkach, które nie wspierają jej natywnie:
// Polyfill dla Array.includes (IE)
if (!Array.prototype.includes) {
Array.prototype.includes = function(searchElement, fromIndex) {
return this.indexOf(searchElement, fromIndex) !== -1;
};
}
// Polyfill dla Object.assign
if (!Object.assign) {
Object.assign = function(target, ...sources) {
if (target == null) {
throw new TypeError('Cannot convert undefined or null to object');
}
const to = Object(target);
for (let index = 0; index < sources.length; index++) {
const nextSource = sources[index];
if (nextSource != null) {
for (const nextKey in nextSource) {
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
};
}
// Intersection Observer Polyfill
(function() {
if ('IntersectionObserver' in window) return;
// Implementacja podstawowego polyfill
window.IntersectionObserver = class {
constructor(callback, options = {}) {
this.callback = callback;
this.options = options;
this.observedElements = new Set();
this.checkIntersections = this.checkIntersections.bind(this);
this.startListening();
}
observe(element) {
this.observedElements.add(element);
this.checkIntersections();
}
unobserve(element) {
this.observedElements.delete(element);
}
disconnect() {
this.observedElements.clear();
this.stopListening();
}
startListening() {
window.addEventListener('scroll', this.checkIntersections);
window.addEventListener('resize', this.checkIntersections);
}
stopListening() {
window.removeEventListener('scroll', this.checkIntersections);
window.removeEventListener('resize', this.checkIntersections);
}
checkIntersections() {
const entries = [];
this.observedElements.forEach(element => {
const rect = element.getBoundingClientRect();
const isIntersecting = (
rect.top < window.innerHeight &&
rect.bottom > 0 &&
rect.left < window.innerWidth &&
rect.right > 0
);
entries.push({
target: element,
isIntersecting,
boundingClientRect: rect,
intersectionRatio: isIntersecting ? 1 : 0
});
});
if (entries.length > 0) {
this.callback(entries);
}
}
};
})();Każdy polyfill najpierw sprawdza, czy funkcja już istnieje, i jeśli tak, niczego nie nadpisuje. Internet Explorer odszedł w 2022 roku, więc wiele takich łatek jest dziś zbędnych.
Automatyzacja testów cross-browser
1. GitHub Actions workflow
Testy Playwrighta uruchomisz automatycznie przy każdym pushu, a macierz tworzy zadanie dla każdego silnika:
# .github/workflows/cross-browser-tests.yml
name: Cross-Browser Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
browser: ['chromium', 'firefox', 'webkit']
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run tests
run: npx playwright test --project=${{ matrix.browser }}
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-${{ matrix.browser }}
path: test-results/Nazwy w macierzy odpowiadają projektom Playwrighta, więc powstają trzy równoległe przebiegi.
2. Jest configuration dla różnych środowisk
Testy jednostkowe w Jest działają w jsdom, czyli symulacji przeglądarki w Node.js, bez prawdziwego silnika renderującego:
// jest.config.js
module.exports = {
projects: [
{
displayName: 'jsdom',
testEnvironment: 'jsdom',
testMatch: ['<rootDir>/src/**/*.test.js'],
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js']
},
{
displayName: 'node',
testEnvironment: 'node',
testMatch: ['<rootDir>/server/**/*.test.js']
}
],
// Global setup dla cross-browser testów
globalSetup: '<rootDir>/test/global-setup.js',
globalTeardown: '<rootDir>/test/global-teardown.js'
};
// setupTests.js - polyfills dla testów
import 'intersection-observer';
import 'whatwg-fetch';
// Mock dla matchMedia (nie wspierane w jsdom)
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});jsdom nie liczy układu strony i nie zna matchMedia, dlatego ją mockujemy, a wygląd CSS sprawdzasz w prawdziwych przeglądarkach.
Debugowanie problemów cross-browser
1. DevTools differences
Przy debugowaniu przydaje się bezpieczny logger i tabela funkcji wspieranych przez przeglądarkę:
// Uniwersalne debugowanie
const Logger = {
log: (...args) => {
if (console && console.log) {
console.log(...args);
}
},
warn: (...args) => {
if (console && console.warn) {
console.warn(...args);
} else if (console && console.log) {
console.log('WARNING:', ...args);
}
},
error: (...args) => {
if (console && console.error) {
console.error(...args);
} else if (console && console.log) {
console.log('ERROR:', ...args);
}
},
table: (data) => {
if (console && console.table) {
console.table(data);
} else {
Logger.log('TABLE:', JSON.stringify(data, null, 2));
}
}
};
// Browser detection dla debugowania
const BrowserDetector = {
isIE: () => navigator.userAgent.indexOf('MSIE') !== -1 || navigator.userAgent.indexOf('Trident') !== -1,
isEdge: () => navigator.userAgent.indexOf('Edge') !== -1,
isChrome: () => navigator.userAgent.indexOf('Chrome') !== -1 && navigator.userAgent.indexOf('Edge') === -1,
isFirefox: () => navigator.userAgent.indexOf('Firefox') !== -1,
isSafari: () => navigator.userAgent.indexOf('Safari') !== -1 && navigator.userAgent.indexOf('Chrome') === -1,
getInfo: () => ({
userAgent: navigator.userAgent,
vendor: navigator.vendor,
platform: navigator.platform,
language: navigator.language,
cookieEnabled: navigator.cookieEnabled,
onLine: navigator.onLine
})
};
// Debugging w różnych przeglądarkach
function debugCompatibility() {
Logger.log('Browser Info:', BrowserDetector.getInfo());
// Test CSS features
const cssFeatures = {
flexbox: CSS.supports('display', 'flex'),
grid: CSS.supports('display', 'grid'),
customProperties: CSS.supports('--foo', 'red'),
backdropFilter: CSS.supports('backdrop-filter', 'blur(10px)'),
objectFit: CSS.supports('object-fit', 'cover')
};
Logger.table(cssFeatures);
// Test JavaScript features
const jsFeatures = {
fetch: typeof fetch !== 'undefined',
intersectionObserver: 'IntersectionObserver' in window,
customElements: 'customElements' in window,
serviceWorker: 'serviceWorker' in navigator,
webp: (() => {
const canvas = document.createElement('canvas');
return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
})()
};
Logger.table(jsFeatures);
}
// Wywołaj debugging w development mode
if (process.env.NODE_ENV === 'development') {
debugCompatibility();
}CSS.supports() to najpewniejszy test. Wykrywanie po userAgent jest kruche: nowy Edge przedstawia się jako "Edg", więc isEdge go nie rozpozna.
2. Remote debugging
Błędy z telefonów trudno zobaczyć, dlatego ten skrypt przechwytuje je i wysyła na serwer:
// Remote debugging setup dla urządzeń mobilnych
class RemoteDebugger {
constructor() {
this.logs = [];
this.errors = [];
this.init();
}
init() {
// Przechwytywanie błędów JavaScript
window.addEventListener('error', (event) => {
const error = {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error ? event.error.stack : null,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent
};
this.errors.push(error);
this.sendToRemoteDebugger('error', error);
});
// Przechwytywanie rejected promises
window.addEventListener('unhandledrejection', (event) => {
const error = {
reason: event.reason.toString(),
stack: event.reason.stack,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent
};
this.errors.push(error);
this.sendToRemoteDebugger('rejection', error);
});
// Override console methods
this.interceptConsole();
}
interceptConsole() {
const originalLog = console.log;
const originalError = console.error;
const originalWarn = console.warn;
console.log = (...args) => {
originalLog.apply(console, args);
this.sendToRemoteDebugger('log', args);
};
console.error = (...args) => {
originalError.apply(console, args);
this.sendToRemoteDebugger('error', args);
};
console.warn = (...args) => {
originalWarn.apply(console, args);
this.sendToRemoteDebugger('warn', args);
};
}
sendToRemoteDebugger(type, data) {
// Wyślij dane do remote debugging service
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/debug', JSON.stringify({
type,
data,
timestamp: new Date().toISOString(),
url: window.location.href,
userAgent: navigator.userAgent
}));
}
}
getDeviceInfo() {
return {
screen: {
width: screen.width,
height: screen.height,
availWidth: screen.availWidth,
availHeight: screen.availHeight,
pixelRatio: window.devicePixelRatio
},
viewport: {
width: window.innerWidth,
height: window.innerHeight
},
browser: navigator.userAgent,
language: navigator.language,
platform: navigator.platform,
touchSupport: 'ontouchstart' in window,
orientation: screen.orientation ? screen.orientation.angle : 'unknown'
};
}
}
// Inicjalizacja tylko w development
if (process.env.NODE_ENV === 'development') {
new RemoteDebugger();
}Do ręcznego debugowania masz też narzędzia wbudowane: Safari na Macu podłączysz do iPhone'a, a Chrome do Androida przez chrome://inspect.
Best practices
1. Testowanie na prawdziwych urządzeniach
Emulator nie odda dotyku, wydajności ani czcionek systemowych, dlatego testuj też na prawdziwych urządzeniach:
// Device farm setup
const testDevices = [
{ name: 'iPhone 13', browser: 'Safari', os: 'iOS 15' },
{ name: 'Samsung Galaxy S21', browser: 'Chrome', os: 'Android 11' },
{ name: 'iPad Pro', browser: 'Safari', os: 'iPadOS 15' },
{ name: 'Desktop', browser: 'Chrome', os: 'Windows 10' },
{ name: 'Desktop', browser: 'Firefox', os: 'Windows 10' },
{ name: 'Desktop', browser: 'Safari', os: 'macOS Big Sur' }
];
// Automatyczne testowanie na device farm
async function runDeviceFarmTests() {
for (const device of testDevices) {
try {
await runTestOnDevice(device);
console.log(`Tests passed on ${device.name} - ${device.browser}`);
} catch (error) {
console.error(`Tests failed on ${device.name} - ${device.browser}:`, error);
}
}
}Listę urządzeń dobieraj według danych o swoich użytkownikach.
2. Performance considerations
Wydajność też bywa różna, a ten monitor zbiera czasy ładowania i metryki Core Web Vitals:
// Monitoring wydajności cross-browser
class PerformanceMonitor {
constructor() {
this.metrics = {};
this.init();
}
init() {
if ('performance' in window) {
this.collectNavigationMetrics();
this.collectResourceMetrics();
this.observeLargestContentfulPaint();
this.observeFirstInputDelay();
}
}
collectNavigationMetrics() {
if (performance.getEntriesByType) {
const navigation = performance.getEntriesByType('navigation')[0];
if (navigation) {
this.metrics.navigation = {
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
domInteractive: navigation.domInteractive - navigation.fetchStart,
firstByte: navigation.responseStart - navigation.requestStart
};
}
}
}
collectResourceMetrics() {
if (performance.getEntriesByType) {
const resources = performance.getEntriesByType('resource');
this.metrics.resources = resources.map(resource => ({
name: resource.name,
duration: resource.duration,
size: resource.transferSize,
type: resource.initiatorType
}));
}
}
observeLargestContentfulPaint() {
if ('PerformanceObserver' in window) {
try {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
this.metrics.lcp = lastEntry.startTime;
});
observer.observe({ entryTypes: ['largest-contentful-paint'] });
} catch (e) {
// Fallback for browsers that don't support LCP
console.log('LCP not supported');
}
}
}
observeFirstInputDelay() {
if ('PerformanceObserver' in window) {
try {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach(entry => {
this.metrics.fid = entry.processingStart - entry.startTime;
});
});
observer.observe({ entryTypes: ['first-input'] });
} catch (e) {
// Fallback for browsers that don't support FID
console.log('FID not supported');
}
}
}
report() {
return {
...this.metrics,
browser: navigator.userAgent,
timestamp: new Date().toISOString()
};
}
}
// Użycie
const monitor = new PerformanceMonitor();
window.addEventListener('load', () => {
setTimeout(() => {
const report = monitor.report();
// Wyślij raport do analytics
console.log('Performance Report:', report);
}, 1000);
});Od marca 2024 roku w Core Web Vitals miejsce FID zajęła metryka INP (Interaction to Next Paint).
Podsumowanie
Testowanie cross-browser to:
- Systematyczny proces zapewniający kompatybilność aplikacji
- Kombinacja narzędzi automatycznych i manualnych testów
- Strategia progressive enhancement dla lepszego UX
- Monitoring rzeczywistych użytkowników na różnych platformach
- Ciągły proces wymagający regularnych aktualizacji
Kluczem do sukcesu jest wczesne planowanie wsparcia przeglądarek, automatyzacja testów i monitorowanie wydajności w różnych środowiskach. Moja rada: zanim użyjesz nowej funkcji, sprawdź ją na caniuse.com i przetestuj w trzech silnikach.
Pamiętaj: dobra świątynia stoi tak samo pewnie w każdym mieście, a dobra strona działa w każdej przeglądarce.
Kod do tej lekcji: index.html
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Scroll Snap Gallery</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Scroll Snap - Galeria Egipskich Komnat</h1>
<section>
<h2>Galeria pozioma (x mandatory)</h2>
<div class="gallery-horizontal">
<div class="gallery-item" style="background: linear-gradient(135deg, #d4af37, #8b6914)">
<h3>Komnata Faraona</h3>
<p>Zlote skarby i hieroglify</p>
</div>
<div class="gallery-item" style="background: linear-gradient(135deg, #1e3a5f, #30d5c8)">
<h3>Sala Tronu</h3>
<p>Lapis lazuli i turkus</p>
</div>
<div class="gallery-item" style="background: linear-gradient(135deg, #b87333, #d4a574)">
<h3>Skarbiec</h3>
<p>Miedziane naczynia rytualne</p>
</div>
<div class="gallery-item" style="background: linear-gradient(135deg, #8b6914, #c9a66b)">
<h3>Biblioteka Papirusow</h3>
<p>Zwoje madrosci Totha</p>
</div>
<div class="gallery-item" style="background: linear-gradient(135deg, #1a1a2e, #3d2914)">
<h3>Krypta</h3>
<p>Tajemnice starozytnosci</p>
</div>
</div>
</section>
<section>
<h2>Galeria pionowa (y mandatory)</h2>
<div class="gallery-vertical">
<div class="v-item" style="background: #d4af37">Poziom 1 - Wejscie</div>
<div class="v-item" style="background: #8b6914">Poziom 2 - Korytarz</div>
<div class="v-item" style="background: #1e3a5f; color: #d4af37">Poziom 3 - Komnata</div>
<div class="v-item" style="background: #1a1a2e; color: #d4af37">Poziom 4 - Sarkofag</div>
</div>
</section>
</body>
</html>Hasła z dokumentacji w tej lekcji
Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Którego silnika renderującego używa przeglądarka Safari?
2. Na czym polega strategia progressive enhancement?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Układanie w pionie
Ułóż deklaracje transform z prefiksami tak, aby standardowa wersja była na końcu (fallback przed standardem):