Chapter 08: Build Optimization and Deployment

Haiyue
19min
Learning Objectives
  • Configure production environment build optimization
  • Implement code splitting and lazy loading strategies
  • Optimize images, fonts, and other static assets
  • Master various deployment methods (Vercel, Netlify, self-hosting)

Knowledge Points

Build Optimization Strategy

Astro’s build optimization revolves around the following core principles:

  • Zero JavaScript by default: Only send JavaScript when needed
  • Static-first: Generate static HTML whenever possible
  • Asset optimization: Automatically compress and optimize various assets
  • Progressive enhancement: Ensure core functionality works without JavaScript

Performance Optimization Diagram

🔄 正在渲染 Mermaid 图表...

Build Configuration Optimization

Astro Configuration File

// astro.config.mjs
import { defineConfig } from 'astro/config';
import vue from '@astrojs/vue';
import tailwind from '@astrojs/tailwind';
import sitemap from '@astrojs/sitemap';
import robotsTxt from 'astro-robots-txt';
import { resolve } from 'path';

export default defineConfig({
  site: 'https://your-domain.com',

  // Build configuration
  build: {
    // Inline assets smaller than 4kb
    inlineStylesheets: 'auto',
    // Asset splitting threshold
    assetsPrefix: '/assets/',
  },

  // Output configuration
  output: 'static', // 'static' | 'server' | 'hybrid'

  // Vite configuration
  vite: {
    // Build optimization
    build: {
      // Code splitting configuration
      rollupOptions: {
        output: {
          // Manual chunk splitting for third-party libraries
          manualChunks: {
            'vue-vendor': ['vue', 'pinia'],
            'ui-vendor': ['@headlessui/vue', '@heroicons/vue'],
          },
        },
      },
      // Compression configuration
      minify: 'terser',
      terserOptions: {
        compress: {
          drop_console: true,
          drop_debugger: true,
        },
      },
    },

    // Dev server configuration
    server: {
      host: true,
      port: 3000,
    },

    // Alias configuration
    resolve: {
      alias: {
        '@': resolve('./src'),
        '@components': resolve('./src/components'),
        '@layouts': resolve('./src/layouts'),
        '@pages': resolve('./src/pages'),
        '@stores': resolve('./src/stores'),
        '@utils': resolve('./src/utils'),
      },
    },

    // CSS configuration
    css: {
      devSourcemap: true,
    },
  },

  // Integration configuration
  integrations: [
    vue({
      // Vue configuration
      template: {
        compilerOptions: {
          // Remove comments in production
          comments: false,
        },
      },
    }),

    tailwind({
      // Tailwind configuration
      config: {
        content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
      },
    }),

    sitemap({
      // Sitemap configuration
      changefreq: 'weekly',
      priority: 0.7,
      lastmod: new Date(),
    }),

    robotsTxt({
      // robots.txt configuration
      policy: [
        {
          userAgent: '*',
          allow: '/',
          disallow: ['/admin', '/api'],
        },
      ],
      sitemap: true,
    }),
  ],

  // Markdown configuration
  markdown: {
    // Code highlighting
    shikiConfig: {
      theme: 'github-dark',
      wrap: true,
    },
    // Plugin configuration
    remarkPlugins: [],
    rehypePlugins: [],
  },

  // Prefetch configuration
  prefetch: {
    prefetchAll: true,
    defaultStrategy: 'hover',
  },

  // Experimental features
  experimental: {
    contentCollectionCache: true,
  },
});

Environment Variable Configuration

# .env.production
PUBLIC_SITE_URL=https://your-domain.com
PUBLIC_GA_ID=G-XXXXXXXXXX
PUBLIC_API_BASE_URL=https://api.your-domain.com

# Build optimization
VITE_DROP_CONSOLE=true
VITE_BUNDLE_ANALYZER=false
// src/env.d.ts
/// <reference types="astro/client" />

interface ImportMetaEnv {
  readonly PUBLIC_SITE_URL: string;
  readonly PUBLIC_GA_ID: string;
  readonly PUBLIC_API_BASE_URL: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}

Asset Optimization

Image Optimization

---
// src/components/OptimizedImage.astro

export interface Props {
  src: string;
  alt: string;
  width?: number;
  height?: number;
  quality?: number;
  format?: 'webp' | 'avif' | 'jpeg' | 'png';
  loading?: 'lazy' | 'eager';
  sizes?: string;
  class?: string;
}

const {
  src,
  alt,
  width = 800,
  height,
  quality = 80,
  format = 'webp',
  loading = 'lazy',
  sizes = '(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw',
  class: className,
} = Astro.props;

// Generate images at different sizes
const widths = [400, 800, 1200, 1600];
const aspectRatio = height ? width / height : undefined;

function generateSrcSet(baseSrc: string, widths: number[]): string {
  return widths
    .map(w => {
      const h = aspectRatio ? Math.round(w / aspectRatio) : undefined;
      const params = new URLSearchParams({
        w: w.toString(),
        ...(h && { h: h.toString() }),
        q: quality.toString(),
        fm: format,
      });
      return `${baseSrc}?${params} ${w}w`;
    })
    .join(', ');
}

const srcSet = generateSrcSet(src, widths);
const optimizedSrc = `${src}?w=${width}&h=${height || ''}&q=${quality}&fm=${format}`;
---

<picture class={className}>
  <!-- WebP format -->
  <source
    srcset={generateSrcSet(src, widths)}
    sizes={sizes}
    type="image/webp"
  />

  <!-- AVIF format (if supported) -->
  {format === 'avif' && (
    <source
      srcset={generateSrcSet(src.replace(/\?.*/, '?fm=avif'), widths)}
      sizes={sizes}
      type="image/avif"
    />
  )}

  <!-- Fallback image -->
  <img
    src={optimizedSrc}
    srcset={srcSet}
    sizes={sizes}
    alt={alt}
    width={width}
    height={height}
    loading={loading}
    decoding="async"
    style={aspectRatio ? `aspect-ratio: ${width}/${height}` : undefined}
  />
</picture>

<style>
picture img {
  @apply w-full h-auto;
}
</style>

Font Optimization

---
// src/layouts/BaseLayout.astro
---

<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />

  <!-- Font preload -->
  <link
    rel="preload"
    href="/fonts/inter-var.woff2"
    as="font"
    type="font/woff2"
    crossorigin
  />

  <!-- Font optimization -->
  <link rel="preconnect" href="https://fonts.googleapis.com" />
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
  <link
    href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
    rel="stylesheet"
  />

  <style>
    /* Font fallback strategy */
    @font-face {
      font-family: 'Inter var';
      src: url('/fonts/inter-var.woff2') format('woff2');
      font-weight: 300 700;
      font-style: normal;
      font-display: swap;
    }

    :root {
      --font-sans: 'Inter var', 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    }

    body {
      font-family: var(--font-sans);
    }
  </style>
</head>
<body>
  <slot />
</body>
</html>

CSS Optimization

/* src/styles/critical.css - Critical path CSS */

/* Basic reset and layout */
*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  tab-size: 4;
}

body {
  margin: 0;
  font-family: var(--font-sans);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Critical layout styles */
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 1rem;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Performance-optimized animations */
@media (prefers-reduced-motion: no-preference) {
  .animate-fade-in {
    animation: fadeIn 0.3s ease-out;
  }

  @keyframes fadeIn {
    from { opacity: 0; transform: translateY(10px); }
    to { opacity: 1; transform: translateY(0); }
  }
}

/* Styles that reduce reflows */
.gpu-accelerated {
  transform: translateZ(0);
  will-change: transform;
}

Code Splitting Strategy

Route-level Splitting

---
// src/pages/admin/[...admin].astro

// Dynamic import admin interface
const AdminApp = () => import('../../components/admin/AdminApp.vue');
---

<html>
<head>
  <title>Admin Dashboard</title>
</head>
<body>
  <!-- Only load admin-related code on admin pages -->
  <div id="admin-app" data-component="AdminApp"></div>

  <script>
    // Lazy load admin app
    async function loadAdminApp() {
      const { createApp } = await import('vue');
      const { default: AdminApp } = await import('../../components/admin/AdminApp.vue');
      const { pinia } = await import('../../stores');

      const app = createApp(AdminApp);
      app.use(pinia);
      app.mount('#admin-app');
    }

    // Check user permission before loading
    fetch('/api/auth/check-admin')
      .then(res => res.ok ? loadAdminApp() : window.location.href = '/login')
      .catch(() => window.location.href = '/login');
  </script>
</body>
</html>

Component-level Lazy Loading

<!-- src/components/LazyDashboard.vue -->
<template>
  <div class="dashboard">
    <Suspense>
      <template #default>
        <div class="dashboard-grid">
          <!-- Async components -->
          <AsyncChartComponent :data="chartData" />
          <AsyncDataTable :items="tableData" />
          <AsyncUserStats :stats="userStats" />
        </div>
      </template>

      <template #fallback>
        <div class="loading-skeleton">
          <div class="skeleton-item"></div>
          <div class="skeleton-item"></div>
          <div class="skeleton-item"></div>
        </div>
      </template>
    </Suspense>
  </div>
</template>

<script setup lang="ts">
import { defineAsyncComponent, ref, onMounted } from 'vue';

// Async component definitions
const AsyncChartComponent = defineAsyncComponent(() =>
  import('./charts/ChartComponent.vue')
);

const AsyncDataTable = defineAsyncComponent(() =>
  import('./tables/DataTable.vue')
);

const AsyncUserStats = defineAsyncComponent(() =>
  import('./stats/UserStats.vue')
);

// Data
const chartData = ref([]);
const tableData = ref([]);
const userStats = ref({});

// Data fetching
onMounted(async () => {
  // Fetch data in parallel
  const [charts, tables, stats] = await Promise.all([
    fetch('/api/charts').then(r => r.json()),
    fetch('/api/tables').then(r => r.json()),
    fetch('/api/stats').then(r => r.json()),
  ]);

  chartData.value = charts;
  tableData.value = tables;
  userStats.value = stats;
});
</script>

<style scoped>
.dashboard-grid {
  @apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6;
}

.loading-skeleton {
  @apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6;
}

.skeleton-item {
  @apply bg-gray-200 rounded-lg h-64 animate-pulse;
}
</style>

Deployment Configuration

Vercel Deployment

// vercel.json
{
  "framework": "astro",
  "buildCommand": "npm run build",
  "outputDirectory": "dist",
  "installCommand": "npm install",
  "devCommand": "npm run dev",
  "functions": {
    "src/pages/api/**/*.ts": {
      "runtime": "nodejs18.x"
    }
  },
  "rewrites": [
    {
      "source": "/api/(.*)",
      "destination": "/api/$1"
    }
  ],
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "X-Content-Type-Options",
          "value": "nosniff"
        },
        {
          "key": "X-Frame-Options",
          "value": "DENY"
        },
        {
          "key": "X-XSS-Protection",
          "value": "1; mode=block"
        }
      ]
    },
    {
      "source": "/assets/(.*)",
      "headers": [
        {
          "key": "Cache-Control",
          "value": "public, max-age=31536000, immutable"
        }
      ]
    }
  ]
}

Netlify Deployment

# netlify.toml
[build]
  command = "npm run build"
  publish = "dist"

[build.environment]
  NODE_VERSION = "18"

[[redirects]]
  from = "/api/*"
  to = "/.netlify/functions/api/:splat"
  status = 200

[[headers]]
  for = "/*"
  [headers.values]
    X-Frame-Options = "DENY"
    X-XSS-Protection = "1; mode=block"
    X-Content-Type-Options = "nosniff"
    Referrer-Policy = "strict-origin-when-cross-origin"

[[headers]]
  for = "/assets/*"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

# Form handling
[[forms]]
  name = "contact"

# Edge Functions
[[edge_functions]]
  function = "auth"
  path = "/admin/*"

Docker Deployment

# Dockerfile
FROM node:18-alpine AS builder

WORKDIR /app

# Copy package files
COPY package*.json ./
RUN npm ci --only=production

# Copy source code
COPY . .

# Build application
RUN npm run build

# Production image
FROM nginx:alpine AS runtime

# Copy build results
COPY --from=builder /app/dist /usr/share/nginx/html

# Copy Nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]
# nginx.conf
events {
    worker_connections 1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/javascript
        application/xml+rss
        application/json;

    server {
        listen 80;
        server_name localhost;

        root /usr/share/nginx/html;
        index index.html;

        # Asset caching
        location /assets/ {
            expires 1y;
            add_header Cache-Control "public, immutable";
        }

        # SPA routing
        location / {
            try_files $uri $uri/ /index.html;
        }

        # Security headers
        add_header X-Frame-Options DENY;
        add_header X-XSS-Protection "1; mode=block";
        add_header X-Content-Type-Options nosniff;
    }
}

GitHub Actions CI/CD

# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [main]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Build application
        run: npm run build
        env:
          PUBLIC_SITE_URL: ${{ secrets.SITE_URL }}
          PUBLIC_GA_ID: ${{ secrets.GA_ID }}

      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.ORG_ID }}
          vercel-project-id: ${{ secrets.PROJECT_ID }}
          vercel-args: '--prod'

Performance Monitoring

Web Vitals Monitoring

// src/lib/analytics.ts

interface WebVital {
  name: string;
  value: number;
  id: string;
}

class PerformanceMonitor {
  private vitals: WebVital[] = [];

  init() {
    if (typeof window === 'undefined') return;

    // Monitor core Web Vitals
    this.measureCLS();
    this.measureFID();
    this.measureLCP();
    this.measureFCP();
    this.measureTTFB();
  }

  private measureCLS() {
    import('web-vitals').then(({ getCLS }) => {
      getCLS((metric) => {
        this.reportVital({
          name: 'CLS',
          value: metric.value,
          id: metric.id,
        });
      });
    });
  }

  private measureFID() {
    import('web-vitals').then(({ getFID }) => {
      getFID((metric) => {
        this.reportVital({
          name: 'FID',
          value: metric.value,
          id: metric.id,
        });
      });
    });
  }

  private measureLCP() {
    import('web-vitals').then(({ getLCP }) => {
      getLCP((metric) => {
        this.reportVital({
          name: 'LCP',
          value: metric.value,
          id: metric.id,
        });
      });
    });
  }

  private measureFCP() {
    import('web-vitals').then(({ getFCP }) => {
      getFCP((metric) => {
        this.reportVital({
          name: 'FCP',
          value: metric.value,
          id: metric.id,
        });
      });
    });
  }

  private measureTTFB() {
    import('web-vitals').then(({ getTTFB }) => {
      getTTFB((metric) => {
        this.reportVital({
          name: 'TTFB',
          value: metric.value,
          id: metric.id,
        });
      });
    });
  }

  private reportVital(vital: WebVital) {
    this.vitals.push(vital);

    // Send to analytics service
    if (import.meta.env.PUBLIC_GA_ID) {
      gtag('event', vital.name, {
        value: Math.round(vital.value),
        metric_id: vital.id,
        custom_parameter: vital.value,
      });
    }

    // Also send to custom analytics endpoint
    fetch('/api/analytics/vitals', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(vital),
    }).catch(console.error);
  }
}

export const performanceMonitor = new PerformanceMonitor();
Key Takeaways
  1. Build Optimization: Properly configure Astro and Vite build options
  2. Asset Optimization: Comprehensive optimization strategy for images, fonts, and CSS
  3. Code Splitting: Smart route-level and component-level splitting
  4. Deployment Strategy: Best practices for multi-platform deployment
  5. Performance Monitoring: Continuously monitor and optimize Web Vitals metrics
Considerations
  • Excessive code splitting may increase network request count
  • Balance image quality and loading speed in optimization
  • Consider content update frequency in caching strategy
  • Regularly analyze and optimize performance monitoring data