Skip to main content

Building Good Websites - Performance, Security, Accessibility Tips

By · · 11 min read

Building Websites That Don't Suck - Practical Developer Tips

Introduction: My First Website Was Slow and Broken

I built my first real website. Looked great. But it was slow. Users didn't stay. I wasn't thinking about performance or security. Just got it working.

Then I started building with my friends who knew web development. They'd point out: "This request takes 3 seconds for nothing." "Your site breaks on their phone." "You're not handling that password securely."

Gradually I realized: making a website that works is easy. Making one that actually works well—fast, secure, accessible, shows up in Google—that's different. It's a bunch of small thoughtful decisions.

This guide covers practical web development stuff: performance, security, accessibility, mobile. Not theory—actual practices that make your websites better.


1. Web Performance Optimization

Core Web Vitals

Google's metrics that matter most:

Largest Contentful Paint (LCP): < 2.5 seconds
  ↳ How fast is the main content visible?
  ↳ Measure: Time until largest visual element renders

First Input Delay (FID): < 100 milliseconds
  ↳ How fast does page respond to user interaction?
  ↳ Measure: Time from click to handler execution
  ↳ Replace by INP (Interaction to Next Paint)

Cumulative Layout Shift (CLS): < 0.1
  ↳ How much does page layout shift unexpectedly?
  ↳ Measure: Total shifts × distance shifted
  ↳ 0.1 = 10% of viewport shifted away

Core Web Vitals Status:
- Good: Green
- Needs Improvement: Yellow
- Poor: Red

Impacts:
- Ranking in search results
- User experience
- Conversion rates

Image Optimization

Problem: Images are 50-80% of page weight

Solution 1: Use Modern Formats

<!-- WebP format (30% smaller than JPEG) -->
<picture>
  <source srcset="image.webp" type="image/webp">
  <source srcset="image.jpg" type="image/jpeg">
  <img src="image.jpg" alt="description">
</picture>

Solution 2: Responsive Images

<!-- Different images for different screen sizes -->
<img 
  srcset="
    image-small.jpg 480w,
    image-medium.jpg 800w,
    image-large.jpg 1200w
  "
  sizes="
    (max-width: 600px) 480px,
    (max-width: 1000px) 800px,
    1200px
  "
  src="image-large.jpg"
  alt="description"
>

Solution 3: Lazy Loading

<!-- Don't load images until user scrolls near them -->
<img 
  src="image.jpg" 
  loading="lazy"
  alt="description"
>

<!-- Manual lazy loading with Intersection Observer -->
<img data-src="image.jpg" class="lazy-image" alt="description">

<script>
const imageObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.src = entry.target.dataset.src;
      imageObserver.unobserve(entry.target);
    }
  });
});

document.querySelectorAll('.lazy-image').forEach(img => {
  imageObserver.observe(img);
});
</script>

CSS and JavaScript Optimization

Minimize Render Blocking

<!-- Render-blocking CSS (blocks page display) -->
<link rel="stylesheet" href="styles.css">

<!-- Non-blocking CSS (load asynchronously) -->
<link rel="stylesheet" href="print.css" media="print">
<link rel="preload" href="styles.css" as="style">

<!-- Render-blocking JS (blocks parsing) -->
<script src="app.js"></script>

<!-- Non-blocking JS (defer loading) -->
<script src="app.js" defer></script>

<!-- Non-blocking JS (load async) -->
<script src="analytics.js" async></script>

Resource Hints

<!-- Tell browser about future actions -->
<link rel="dns-prefetch" href="//cdn.example.com">
<link rel="preconnect" href="//api.example.com">
<link rel="prefetch" href="//next-page.example.com">
<link rel="preload" href="fonts/main.woff2" as="font" crossorigin>

Critical Rendering Path

1. HTML parsing
   ↓
2. CSS download + parsing (render blocking)
   ↓
3. DOM ready
   ↓
4. JavaScript execution
   ↓
5. Browser paint (page visible)
   ↓
6. JavaScript finishes (page interactive)

Optimization:

<!-- Put critical CSS inline -->
<style>
  /* Critical above-fold CSS */
  body { margin: 0; }
  .header { background: blue; }
</style>

<!-- Defer non-critical CSS -->
<link rel="stylesheet" href="theme.css" media="print" onload="this.media='all'">

<!-- Move scripts to bottom (they execute after parse) -->
<body>
  <!-- content -->
  <script src="app.js"></script>
</body>

Caching Strategies

Browser Cache:
  Static assets cached for 1 year
  Let browser handle with ETags
  User never downloads again

CDN Cache:
  Content cached worldwide
  Miss = fetch from origin
  Hit = serve from edge server
  Cache for 1 hour, purge on deploy

Server Cache:
  In-memory cache (Redis)
  Database query results
  10x faster than DB queries

Multi-layer example:
  Browser → CDN → Server → Database
  Hit t1 → CDN t10 → DB t100

2. Security Best Practices

Input Validation and Sanitization

Never trust user input

// VULNERABLE: SQL Injection
const email = req.query.email;
const query = `SELECT * FROM users WHERE email = '${email}'`;
// email = "' OR '1'='1" bypasses authentication!

// SECURE: Parameterized queries
const query = 'SELECT * FROM users WHERE email = ?';
db.query(query, [email]);

// VULNERABLE: XSS (Cross-Site Scripting)
document.innerHTML = `<p>${userComment}</p>`;
// Comment = "<img src=x onerror='stealCookies()'>"

// SECURE: Text nodes or sanitization
const p = document.createElement('p');
p.textContent = userComment;
document.appendChild(p);

// Or use DOMPurify library
const cleanHTML = DOMPurify.sanitize(userComment);

HTTPS and SSL/TLS

Why HTTPS?
- Encrypts data in transit
- Prevents man-in-the-middle attacks
- Browser shows trust indicator
- Required for secure payments
- Google ranks HTTPS higher

Implementation:
1. Buy SSL/TLS certificate
2. Point domain DNS to server
3. Configure certificate on server
4. Redirect HTTP to HTTPS

Certificate Types:
- Self-signed: Free, not trusted
- Domain-validated: $10-50/year
- Organization-validated: $50-200/year
- Extended-validation: $200+/year

Modern: Use Let's Encrypt (free, automated)

Content Security Policy (CSP)

CSP prevents XSS attacks by controlling which resources load

Header: Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'

Rules:
- default-src 'self'
  Only load resources from own domain
  
- script-src 'self' https://cdn.example.com
  Only scripts from own domain or CDN
  
- style-src 'unsafe-inline'
  Allow inline CSS (required for performance)
  
- img-src 'self' data: https:
  Allow from own domain, data URLs, any HTTPS

Report-uri /csp-report
  Send violations to this endpoint for monitoring

Authentication and Authorization

// Secure password hashing (bcrypt)
const bcrypt = require('bcrypt');

// During registration
const hashedPassword = await bcrypt.hash(password, 10); // 10 = rounds
await db.users.insert({ email, password: hashedPassword });

// During login
const user = await db.users.findOne({ email });
const isValid = await bcrypt.compare(password, user.password);

// Never store plain passwords!
// Never use MD5 or simple hashing!

// JWT tokens for stateless auth
const jwt = require('jsonwebtoken');

const token = jwt.sign(
  { userId: user.id, email: user.email },
  process.env.JWT_SECRET,
  { expiresIn: '24h' }
);

// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);

Rate Limiting

// Prevent brute force attacks
const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 minute window
  max: 100,                   // 100 requests per window
  message: 'Too many requests',
  statusCode: 429
});

// Apply to all routes
app.use(limiter);

// Specific endpoint (tighter limits)
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,  // Only 5 login attempts
  skip: (req) => req.user // Don't rate limit logged-in users
});

app.post('/login', loginLimiter, (req, res) => {
  // Handle login
});

3. Responsive Web Design

Mobile-First Approach

/* START with mobile styles */
body {
  font-size: 14px;
  margin: 0;
}

.container {
  display: block; /* Full width on mobile */
}

/* THEN add desktop styles */
@media (min-width: 768px) {
  body {
    font-size: 16px;
  }
  
  .container {
    display: grid;
    grid-template-columns: 1fr 3fr;
    gap: 20px;
  }
}

@media (min-width: 1200px) {
  body {
    max-width: 1200px;
    margin: 0 auto;
  }
}

Flexible Layouts

<!-- CSS Grid - Modern layout -->
<div style="
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 20px;
">
  <div>Card 1</div>
  <div>Card 2</div>
  <div>Card 3</div>
</div>

<!-- Automatically adapts: 1 column on mobile, 3 on desktop -->

<!-- Flexbox - Simple alignment -->
<div style="
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
">
  <item style="flex: 1 1 200px;">Item 1</item>
  <item style="flex: 1 1 200px;">Item 2</item>
</div>

Viewport Meta Tag

<!-- REQUIRED for mobile devices -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">

Explanation:
- width=device-width: Page width matches device width
- initial-scale=1.0: No zoom on load
- user-scalable=yes: Allow pinch-zoom

4. Accessibility (A11y)

Semantic HTML

<!-- WRONG: Non-semantic -->
<div class="header">Welcome</div>
<div class="nav">
  <div class="nav-item"><a href="/">Home</a></div>
  <div class="nav-item"><a href="/about">About</a></div>
</div>
<div class="content">
  <div class="article">...</div>
</div>

<!-- CORRECT: Semantic -->
<header>
  <h1>Welcome</h1>
  <nav>
    <a href="/">Home</a>
    <a href="/about">About</a>
  </nav>
</header>
<main>
  <article>...</article>
</main>
<footer>...</footer>

Semantic elements help:
- Screen readers understand page structure
- SEO crawlers understand importance
- Developers understand intent
- Browsers provide built-in features

ARIA Attributes

<!-- ARIA labels for non-semantic elements -->
<button aria-label="Open menu">☰</button>

<div role="alert">
  Please fix the errors below
</div>

<div role="region" aria-labelledby="section-title">
  <h2 id="section-title">Important Section</h2>
  Content here
</div>

<!-- Live regions update without refresh -->
<div aria-live="polite" aria-atomic="true">
  <span id="status">Loading...</span>
</div>

<!-- Expanded/collapsed -->
<button aria-expanded="false" aria-controls="menu">
  Menu
</button>
<menu id="menu" hidden>...</menu>

Color and Contrast

WCAG Standards:
- Normal text: 4.5:1 contrast ratio
- Large text: 3:1 contrast ratio
- Graphical: 3:1 contrast ratio

Examples:
✓ Black (#000000) on white (#FFFFFF) = 21:1 (excellent)
✓ Dark gray (#333333) on white = 12:1 (excellent)
✗ Gray (#AAAAAA) on white = 2.5:1 (fails)
✗ Similar colors fail (red on orange)

Check with: WebAIM Color Contrast Checker

Keyboard Navigation

<!-- Make all interactive elements keyboard accessible -->
<button>Click me</button>
<a href="/">Link</a>
<input type="text">
<select><option>Choose</option></select>

<!-- Use tabindex sparingly -->
<div tabindex="0">Now focusable with keyboard</div>

<!-- NOT keyboard accessible -->
<div class="fake-button">Not accessible!</div>

<!-- Better form accessibility -->
<label for="email">Email:</label>
<input id="email" type="email" required>

<label for="password">Password:</label>
<input id="password" type="password" required>

5. Testing Strategies

Unit Testing

// Jest test example
describe('User class', () => {
  it('should create user with email', () => {
    const user = new User('john@example.com', 'password');
    expect(user.email).toBe('john@example.com');
  });

  it('should validate email format', () => {
    const user = new User('invalid-email', 'password');
    expect(user.isValid()).toBe(false);
  });

  it('should hash password on creation', async () => {
    const user = new User('john@example.com', 'password123');
    const isValid = await user.validatePassword('password123');
    expect(isValid).toBe(true);
  });
});

Integration Testing

// Test API endpoints
describe('User API', () => {
  it('should register new user', async () => {
    const response = await request(app)
      .post('/api/users/register')
      .send({
        email: 'test@example.com',
        password: 'password123'
      });

    expect(response.status).toBe(201);
    expect(response.body.id).toBeDefined();
  });

  it('should reject duplicate email', async () => {
    await request(app)
      .post('/api/users/register')
      .send({ email: 'test@example.com', password: 'pass' });

    const response = await request(app)
      .post('/api/users/register')
      .send({ email: 'test@example.com', password: 'pass2' });

    expect(response.status).toBe(400);
  });
});

E2E Testing

// Cypress test - user perspective
describe('Login flow', () => {
  it('user can login successfully', () => {
    cy.visit('https://example.com/login');
    cy.get('[data-testid="email"]').type('user@example.com');
    cy.get('[data-testid="password"]').type('password123');
    cy.get('button[type="submit"]').click();
    cy.url().should('include', '/dashboard');
  });

  it('shows error for invalid credentials', () => {
    cy.visit('https://example.com/login');
    cy.get('[data-testid="email"]').type('user@example.com');
    cy.get('[data-testid="password"]').type('wrong');
    cy.get('button[type="submit"]').click();
    cy.contains('Invalid credentials').should('be.visible');
  });
});

6. SEO Best Practices

Meta Tags

<head>
  <!-- Title (50-60 characters) -->
  <title>Web Development Fundamentals - Complete Guide 2026</title>
  
  <!-- Description (150-160 characters) -->
  <meta name="description" content="Master web development with this comprehensive guide covering performance, security, accessibility, and modern best practices.">
  
  <!-- Canonical URL (prevent duplicate content) -->
  <link rel="canonical" href="https://example.com/web-development-guide">
  
  <!-- Structured data (helps search engines understand content) -->
  <script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "Article",
    "headline": "Web Development Fundamentals",
    "author": "John Doe",
    "datePublished": "2026-02-21",
    "articleBody": "..."
  }
  </script>
  
  <!-- Open Graph (for social sharing) -->
  <meta property="og:title" content="Web Development Guide">
  <meta property="og:description" content="Master web development">
  <meta property="og:image" content="https://example.com/image.jpg">
  <meta property="og:url" content="https://example.com/web-dev-guide">
</head>

URL Structure

GOOD:
/blog/web-development-guide
/products/laptop-gaming-asus-rog
/about
/services/web-design

BAD:
/blog/article1
/products/product?id=123
/services/abc123
/default.asp?page=about

Heading Hierarchy

<!-- One H1 per page -->
<h1>Web Development Guide</h1>

<!-- Use H2 for main sections -->
<h2>Performance Optimization</h2>
<h2>Security Best Practices</h2>

<!-- Use H3 for subsections -->
<h3>Image Optimization</h3>
<h3>Cache Strategies</h3>

<!-- DON'T skip levels -->
<h1>Title</h1>
<h3>Wrong - should be H2</h3>

Mobile Friendliness

Google's mobile ranking factors:
1. Viewport configuration (responsive meta tag)
2. Readability (font size, tap targets)
3. Performance (mobile page speed)
4. No pop-ups blocking content
5. Structured data for rich results

7. Progressive Web Apps (PWA)

Service Worker

// service-worker.js
const CACHE_VERSION = 'v1';

// Install: Cache static assets
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_VERSION).then((cache) => {
      return cache.addAll([
        '/',
        '/index.html',
        '/styles.css',
        '/app.js',
        '/offline.html'
      ]);
    })
  );
});

// Fetch: Serve from cache, fallback to network
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      return response || fetch(event.request)
        .catch(() => caches.match('/offline.html'));
    })
  );
});

// Register service worker
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js');
}

Web App Manifest

{
  "name": "My Amazing App",
  "short_name": "MyApp",
  "description": "Do amazing things",
  "start_url": "/",
  "scope": "/",
  "display": "standalone",
  "orientation": "portrait",
  "theme_color": "#2196F3",
  "background_color": "#FFFFFF",
  "icons": [
    {
      "src": "/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}
<!-- Link in HTML -->
<link rel="manifest" href="/manifest.json">

8. Monitoring and Analytics

Real User Monitoring (RUM)

// Measure performance in production
const perfData = performance.getEntriesByType('navigation')[0];

console.log('Time to First Byte (TTFB):', perfData.responseStart - perfData.fetchStart);
console.log('DOM Content Loaded:', perfData.domContentLoadedEventEnd - perfData.fetchStart);
console.log('Page Load:', perfData.loadEventEnd - perfData.fetchStart);

// Send to analytics
sendAnalytics({
  ttfb: perfData.responseStart - perfData.fetchStart,
  dcl: perfData.domContentLoadedEventEnd - perfData.fetchStart,
  load: perfData.loadEventEnd - perfData.fetchStart
});

Synthetic Monitoring

Lighthouse - Automated auditing
- Performance (LCP, FID, CLS)
- Accessibility (WCAG compliance)
- Best Practices (security, browser features)
- SEO (metadata, mobile-friendly)
- PWA (offline capability)

Scores: 0-100
- 90+: Good
- 50-89: Needs improvement
- <50: Poor

Run: lighthouse https://example.com --view

Key Takeaways

  1. Performance First - LCP, FID, CLS drive user experience
  2. Security Essential - Input validation, HTTPS, CSP, hashing
  3. Accessibility Mandatory - Semantic HTML, ARIA, keyboard nav
  4. Mobile Priority - Mobile-first design, touch-friendly
  5. Test Thoroughly - Unit, integration, E2E testing
  6. Monitor Always - Track real user metrics
  7. Optimize Continuously - Performance debt kills products