← 中文资源

简体中文版本

Prefetch 与 Preload 的区别

prefetch 以低优先级投机抓取未来导航可能用到的资源,preload 则紧急抓取当前页面必然需要的资源,混用两者会浪费带宽或拖延关键路径。

阅读英文原文

概念定义

prefetch 是低优先级的投机性抓取,目标可能是用户下一步才需要的资源或文档;preload 是为当前导航服务的高优先级抓取。两者契约相反:一个赌未来,一个救现在。speculation rules 进一步把 prefetch 泛化为整页预渲染。

工作机制

浏览器在网络空闲时执行 prefetch,刻意排在紧急工作之后;响应按缓存头存入 HTTP 缓存,后续导航只有在新鲜度规则允许时才能命中。speculation rules 声明 URL 与 eagerness 级别,由浏览器决定抓取或预渲染的激进程度,并可通过分析工具观察触发情况。

使用场景

下一个目的地可预测时收益最大:分页列表、结账第二步、落地页上最常见的跳转链接。控制在少数高概率目标上 prefetch 是廉价的;把所有候选页都 prefetch 则纯属浪费。

常见误区

prefetch 不保证下一次导航瞬时完成,只是预热缓存,仍需命中缓存才有效。no-store 或 TTL 极短的资源 prefetch 后就是浪费的字节。对已认证的 API 端点 prefetch,若缓存键忽略凭证还可能跨会话泄漏状态。上线前后应度量导航启动时间与 prefetch 命中率。

代码示例

package main

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"time"
)

type Shipment struct {
	ID     string `json:"id"`
	Status string `json:"status"`
}

func queryShipment(ctx context.Context, baseURL, id string) (*Shipment, error) {
	ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
	defer cancel() // 必须释放计时器资源

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/shipments/"+id, nil)
	if err != nil {
		return nil, fmt.Errorf("build request: %w", err)
	}
	req.Header.Set("Accept", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		if errors.Is(err, context.DeadlineExceeded) {
			return nil, fmt.Errorf("shipment %s timed out after 3s: %w", id, err)
		}
		return nil, fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	var shipment Shipment
	if err := json.NewDecoder(resp.Body).Decode(&shipment); err != nil {
		return nil, fmt.Errorf("decode body: %w", err)
	}
	return &shipment, nil
}

func main() {
	s, err := queryShipment(context.Background(), "https://api.example.com", "sh-1")
	fmt.Println(s, err)
}
package main

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"time"
)

type fetchResult struct {
	Status int
	Body   string
}

func fetchWithDeadline(ctx context.Context, client *http.Client, url string, timeout time.Duration) (fetchResult, error) {
	ctx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return fetchResult{}, fmt.Errorf("build request: %w", err)
	}

	resp, err := client.Do(req)
	if err != nil {
		if errors.Is(ctx.Err(), context.DeadlineExceeded) {
			return fetchResult{}, fmt.Errorf("fetch %s: deadline exceeded after %s: %w", url, timeout, context.DeadlineExceeded)
		}
		return fetchResult{}, fmt.Errorf("fetch %s: %w", url, err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
	if err != nil {
		return fetchResult{}, fmt.Errorf("read body: %w", err)
	}
	return fetchResult{Status: resp.StatusCode, Body: string(body)}, nil
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/fast", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "ok")
	})
	mux.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) {
		select {
		case <-time.After(200 * time.Millisecond):
			fmt.Fprint(w, "late")
		case <-r.Context().Done():
		}
	})
	server := httptest.NewServer(mux)
	defer server.Close()

	client := &http.Client{Timeout: 5 * time.Second}
	ctx := context.Background()

	result, err := fetchWithDeadline(ctx, client, server.URL+"/fast", 500*time.Millisecond)
	fmt.Printf("fast: status=%d body=%q err=%v\n", result.Status, result.Body, err)

	result, err = fetchWithDeadline(ctx, client, server.URL+"/slow", 50*time.Millisecond)
	fmt.Printf("slow: status=%d body=%q err=%v\n", result.Status, result.Body, err)
	if errors.Is(err, context.DeadlineExceeded) {
		fmt.Println("slow request classified as a deadline error")
	}
}