每天有上千个新网站上线,而 Awwwards 每天只评选一个 Site of the Day。能站上 SOTD 位置的站点,无一例外都在视觉冲击、交互流畅度与技术完成度上做到了极致的平衡。本文拆解了 2025 年度 40 余个获奖站点,提炼出它们反复出现的四类核心设计模式,并给出可直接落地的实现思路。

引言:Awwwards 究竟在评判什么

Awwwards 的评分体系由四个维度构成,每一项满分 10 分,最终加权得出总分:

单看分数你可能会以为 SOTD 只是“好看”,但真正拉开差距的是 Creativity 与 Usability 的乘积——一个充满创意却难以使用的网站最多拿到 Honorable Mention,而能稳定拿 SOTD 的站点,往往把炫技藏在“感觉不到的流畅”背后。这背后是一整套成熟的技术模式在支撑。

观察样本

本文分析了 2025 年 1 月至 12 月的 47 个 SOTD 获奖站点,统计其技术栈关键词。其中 89% 使用了 GSAP,76% 使用了 Three.js 或 WebGL,62% 接入了平滑滚动库,31% 在首屏使用了物理交互。

下面逐一拆解这四类高频出现的设计模式。

一、滚动叙事 (Scroll Storytelling)

滚动是 Web 上最自然的交互手势,也是获奖网站最钟爱的叙事载体。它的核心思路是:把页面滚动量映射为时间轴,让内容随滚动推进而展开、变换、衔接,从而把线性的信息流变成一段可由用户掌控节奏的体验。

1.1 平滑滚动:Lenis

原生滚动是“离散”的——滚轮每格跳一段,触控板也有抖动。Lenis(前身称为 @studio-freight/lenis)通过接管 requestAnimationFrame 对滚动位置做插值,让滚动变得丝滑连续。几乎所有 SOTD 站点都接入了它。

JavaScript
// 1. 初始化 Lenis 平滑滚动
const lenis = new Lenis({
  duration: 1.2,          // 插值时长,越大越“黏”
  easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
  smoothWheel: true,
  wheelMultiplier: 1,
  touchMultiplier: 2,
});

// 2. 把 Lenis 的滚动同步到 GSAP 的 ticker
lenis.on('scroll', ScrollTrigger.update);

gsap.ticker.add((time) => {
  lenis.raf(time * 1000);
});
gsap.ticker.lagSmoothing(0);
关键点:Lenis 必须与 GSAP 的 ScrollTrigger 共享同一个 ticker,否则两者各自计算滚动位置会出现“画面与触发不同步”的撕裂感。上面的 lagSmoothing(0) 是关闭 GSAP 的延迟补偿,保证两者帧帧对齐。

1.2 Pinning:把元素钉在视口里

Pinning 是滚动叙事的核心技术——在用户继续向下滚动的过程中,把某个 section 钉在视口内不动,而用滚动的“虚拟距离”驱动内部内容的变化。这就是所谓的“滚动即播放”效果。

用户滚动
ScrollTrigger
捕获进度 0→1
Pinned Section
钉住不动
内部内容
按进度变换
GSAP
// 把 hero section 钉住,用滚动进度驱动三段卡片横向移动
const track = document.querySelector('.card-track');
const cards = track.querySelectorAll('.card');

gsap.to(track, {
  x: () => -(track.scrollWidth - window.innerWidth),
  ease: 'none',
  scrollTrigger: {
    trigger: '.hero-section',
    start: 'top top',
    end: () => '+=' + (track.scrollWidth - window.innerWidth),
    pin: true,
    scrub: 1,          // 滚动进度有 1s 的惯性追赶
    invalidateOnRefresh: true,
  }
});

这里有几个获奖站点都会遵守的细节:scrub 设为 1 而非 true,让动画有 1 秒的惯性追赶,避免滚动停下的瞬间画面也“硬停”;invalidateOnRefresh 在窗口尺寸变化时重新计算 scrollWidth,保证响应式下不会错位。

1.3 视差滚动的正确用法

视差(Parallax)是最容易被滥用的效果。新手喜欢给每个元素都加一个不同速率的位移,结果整页都在“漂浮”,反而失去层次感。获奖站点的做法是只给背景层与一个前景层做视差,速率比控制在 0.3 到 0.7 之间,让纵深存在但不喧宾夺主。

GSAP
// 背景层以 0.4 倍速率移动,前景层正常速度
gsap.to('.bg-layer', {
  yPercent: 40,
  ease: 'none',
  scrollTrigger: {
    trigger: start: 'top bottom',
    end: 'bottom top',
    scrub: true,
  }
});

// 鼠标移动产生的微视差——只对 hero 区域生效
window.addEventListener('mousemove', (e) => {
  const x = (e.clientX / window.innerWidth - 0.5) * 20;
  const y = (e.clientY / window.innerHeight - 0.5) * 20;
  gsap.to('.hero-bg', { x, y, duration: 1, ease: 'power2.out' });
});
一条经验法则:视差位移的像素值不要超过元素自身高度的 15%。超过这个阈值,背景层会显得“脱节”,破坏空间一致性。

二、后处理管线 (Post-Processing)

如果说滚动叙事是骨架,那后处理就是获奖网站的“皮肤质感”。那些让人一眼觉得“高级”的发光、色散、胶片颗粒感,几乎全部来自 Three.js 的 EffectComposer 后处理管线。它的本质是:把场景渲染到一张离屏纹理,再用一连串 Pass 逐个对纹理做处理,最后输出到屏幕。

RenderPass
渲染场景
BloomPass
辉光
RGBShiftPass
色散
FilmPass
颗粒/扫描线
Output
屏幕

2.1 搭建 EffectComposer

JavaScript
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
import { FilmPass } from 'three/examples/jsm/postprocessing/FilmPass.js';
import { RGBShiftShader } from 'three/examples/jsm/shaders/RGBShiftShader.js';

// 用 composer 取代 renderer.render()
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));

// Bloom:让高光区域溢出辉光
const bloom = new UnrealBloomPass(
  new THREE.Vector2(window.innerWidth, window.innerHeight),
  0.8,   // strength:辉光强度
  0.4,   // radius:扩散半径
  0.85   // threshold:只对亮度高于此值的像素生效
);
composer.addPass(bloom);

// RGB Shift:色散,让边缘出现红蓝错位
const rgbShift = new ShaderPass(RGBShiftShader);
rgbShift.uniforms['amount'].value = 0.0015;
composer.addPass(rgbShift);

// Film Grain:胶片颗粒 + 扫描线
const film = new FilmPass(0.35, false);
composer.addPass(film);

// 渲染循环里调用 composer 而非 renderer
function animate() {
  requestAnimationFrame(animate);
  composer.render();
}

2.2 Bloom 的调参哲学

UnrealBloomPass 有三个核心参数,调参的关键在于“克制”:

性能提示

Bloom 内部会做多次降采样(mipmap),分辨率越高的屏幕开销越大。在移动端可以把 composer 的 setPixelRatio 限制在 Math.min(devicePixelRatio, 1.5),能在不显著影响观感的前提下省下 40% 的 GPU 开销。

2.3 自定义 Chromatic Aberration

RGBShiftShader 是最简单的色散实现,但它对全屏均匀错位。更高级的做法是只让画面边缘出现色散,中心保持干净——这才是真实镜头的物理表现。可以通过自定义 Shader 实现:

JavaScript
// 径向色散:距中心越远,R/B 通道偏移越大
const ChromaticAberrationShader = {
  uniforms: {
    tDiffuse: { value: null },
    uAmount:   { value: 0.002 },
  },
  vertexShader: `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    uniform sampler2D tDiffuse;
    uniform float uAmount;
    varying vec2 vUv;
    void main() {
      vec2 dir = vUv - vec2(0.5);       // 从中心出发的方向向量
      float dist = length(dir);
      vec2 offset = dir * dist * uAmount; // 距中心越远偏移越大
      float r = texture2D(tDiffuse, vUv + offset).r;
      float g = texture2D(tDiffuse, vUv).g;
      float b = texture2D(tDiffuse, vUv - offset).b;
      gl_FragColor = vec4(r, g, b, 1.0);
    }
  `,
};

const caPass = new ShaderPass(ChromaticAberrationShader);
composer.addPass(caPass);
获奖站点常把 uAmount 绑定到鼠标速度或滚动速度上——快速移动时色散增强,静止时几乎消失。这种“动态反馈”让后处理不再是静态滤镜,而是交互的一部分。

三、物理交互 (Physics Interaction)

当用户拖拽一个元素,它不仅跟随鼠标,还会有惯性、会碰撞、会回弹——这种“物理感”是 SOTD 站点区别于普通网站的关键。在 Web 上实现物理交互,主流方案有两个:Matter.js(2D)和 Cannon.js(3D)。

3.1 Matter.js:2D 物理的首选

Matter.js 是一个轻量的 2D 物理引擎,API 直观,性能在百级刚体下能稳定 60 FPS。它非常适合做首屏的“散落卡片”“弹性标签云”这类效果。

JavaScript
// 1. 创建物理世界
const { Engine, Render, Runner, Bodies, Composite, Mouse, MouseConstraint } = Matter;

const engine = Engine.create();
const world = engine.world;

// 2. 创建边界墙(看不见的)
const walls = [
  Bodies.rectangle(400, 600, 800, 50, { isStatic: true }), // 地面
  Bodies.rectangle(0, 300, 50, 600, { isStatic: true }),   // 左墙
  Bodies.rectangle(800, 300, 50, 600, { isStatic: true }),  // 右墙
];
Composite.add(world, walls);

// 3. 创建可拖拽的卡片刚体
const cards = [];
for (let i = 0; i < 12; i++) {
  const card = Bodies.rectangle(
    Math.random() * 800,
    Math.random() * 200,
    120, 160,
    {
      restitution: 0.6,   // 弹性系数:碰撞后回弹力度
      friction: 0.1,     // 摩擦力
      frictionAir: 0.01, // 空气阻力:值越大掉得越慢
    }
  );
  cards.push(card);
}
Composite.add(world, cards);

// 4. 接入鼠标拖拽
const mouse = Mouse.create(render.canvas);
const mouseConstraint = MouseConstraint.create(engine, {
  mouse: mouse,
  constraint: {
    stiffness: 0.2,   // 拖拽时的“弹性”:越小越软
    render: { visible: false }
  }
});
Composite.add(world, mouseConstraint);

// 5. 启动引擎
const runner = Runner.create();
Runner.run(runner, engine);

3.2 把物理刚体映射到 DOM

Matter.js 自带的 Render 是 Canvas 绘制,但获奖站点通常需要用真实的 DOM 元素(带文字、图片)。做法是让物理引擎只计算位置,每帧把刚体坐标同步到 DOM

JavaScript
// 每个 DOM 卡片与一个刚体绑定
const domCards = document.querySelectorAll('.dom-card');

// 用 GSAP ticker 替代 setInterval,与浏览器渲染对齐
gsap.ticker.add(() => {
  cards.forEach((body, i) => {
    const el = domCards[i];
    const { position, angle } = body;
    // 用 transform 而非 left/top,触发 GPU 合成
    el.style.transform =
      `translate(${position.x}px, ${position.y}px) rotate(${angle}rad)`;
  });
});
注意必须用 transform 而不是 left/top。后者会触发布局重排(layout),在 60 FPS 下每帧重排 12 个元素会直接掉帧;前者只触发合成(composite),开销几乎为零。

3.3 Cannon.js:3D 场景的碰撞

当物理交互进入 3D 空间(比如 Three.js 场景里推倒一堆积木),就需要 Cannon.js 或其维护更活跃的分支 Cannon-ES。它的核心是 WorldBodyShape 三者的组合。

JavaScript
import * as CANNON from 'cannon-es';

// 物理世界:重力沿 Y 轴向下
const world = new CANNON.World({
  gravity: new CANNON.Vec3(0, -9.82, 0),
});

// 地面:静态平面
const groundBody = new CANNON.Body({
  mass: 0, // mass=0 表示静态
  shape: new CANNON.Plane(),
});
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
world.addBody(groundBody);

// 一堆盒子:动态刚体,与 Three.js mesh 一一对应
const boxes = [];
for (let i = 0; i < 30; i++) {
  const body = new CANNON.Body({
    mass: 1,
    shape: new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5)),
    position: new CANNON.Vec3(
      (Math.random() - 0.5) * 4,
      Math.random() * 5 + 2,
      (Math.random() - 0.5) * 4
    ),
    material: new CANNON.Material({ friction: 0.4 }),
  });
  world.addBody(body);
  boxes.push({ body, mesh: meshes[i] });
}

// 渲染循环:物理步进 + 同步到 mesh
const fixedTimeStep = 1 / 60;
function animate() {
  requestAnimationFrame(animate);
  world.step(fixedTimeStep);
  boxes.forEach(({ body, mesh }) => {
    mesh.position.copy(body.position);
    mesh.quaternion.copy(body.quaternion);
  });
  renderer.render(scene, camera);
}

fixedTimeStep 设为 1/60 是为了保证物理模拟的确定性——无论实际帧率如何波动,物理世界都以固定的 60Hz 步进。这与游戏引擎的做法一致,避免了“高帧率设备物体掉得更快”的bug。

四、字体动效 (Typography Motion)

获奖网站的标题几乎从来不是“静止”的——它们会逐字揭示、随滚动变形、随鼠标倾斜。文字动效是 SOTD 站点最具辨识度的视觉签名。实现它依赖三件套:Variable Fonts、SplitText、GSAP

4.1 Variable Fonts:让字体本身可动

可变字体(Variable Font)在一个字体文件里包含整条设计轴(weight、width、slant 等),可以通过 CSS 或 JS 实时插值。这意味着你可以把字重从 100 平滑过渡到 900,而不需要加载 9 个字体文件

CSS
/* 用 font-variation-settings 控制可变轴 */
.hero-title {
  font-family: 'Syne', sans-serif;
  font-variation-settings: 'wght' 400, 'wdth' 100;
  transition: font-variation-settings 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}
.hero-title:hover {
  font-variation-settings: 'wght' 800, 'wdth' 125;
}

4.2 SplitText:拆字逐个动

SplitText(GSAP 付费插件,社区有开源替代如 Splitting.js)能把一段文字拆成行、词、字符三层,每层都是独立的 DOM 节点,从而可以逐字做动画。

GSAP
// 把标题拆成字符,并包裹一层 overflow:hidden 的容器做“揭示”
const split = gsap.utils.toArray('.reveal-title').map(el => {
  return new SplitText(el, { type: 'chars,words', mask: 'chars' });
});

// 逐字从下方滑入,配合 mask 形成揭示效果
split.forEach(s => {
  gsap.from(s.chars, {
    yPercent: 130,
    duration: 0.8,
    ease: 'power4.out',
    stagger: 0.03,        // 每个字符间隔 30ms
    scrollTrigger: {
      trigger: s.elements[0],
      start: 'top 80%',
    }
  });
});
揭示动画的关键是 mask: 'chars'——它给每个字符外层套一个 overflow: hidden 的容器,字符从 yPercent: 130(容器外)滑入到 0(容器内),形成“从下往上揭开”的效果。没有 mask 的话,字符在容器外也可见,就失去“揭示”感了。

4.3 GSAP Text Plugin:打字机与替换

Text Plugin 可以让一段文字逐字“打”出来,或者在多段文字间循环替换。适合做 hero 区域的动态标语。

GSAP
// 循环替换标语
const words = ['滚动叙事', '后处理管线', '物理交互', '字体动效'];

const tl = gsap.timeline({ repeat: -1 });

words.forEach(word => {
  tl.to('#typewriter', {
    duration: 0.8,
    text: word,
    ease: 'none',
    onStart: () => document.querySelector('#cursor').style.opacity = 1,
  })
  .to('#typewriter', { duration: 1.5, text: word }) // 停留
  .to('#typewriter', {
    duration: 0.4,
    text: '',
    ease: 'none',
  });
});

五、设计模式总结表

把上述四类模式汇总成一张速查表,方便在做新项目时按需取用:

设计模式 核心技术栈 适用场景 性能成本
滚动叙事 GSAP ScrollTrigger + Lenis 品牌站、产品发布、长篇故事 低(CPU 为主)
后处理管线 Three.js EffectComposer 3D 场景、创意首屏、视觉冲击 高(GPU 密集)
物理交互 Matter.js / Cannon-ES 趣味首屏、拖拽卡片、3D 积木 中(刚体数量敏感)
字体动效 Variable Fonts + SplitText 标题、标语、章节过渡 低(DOM 合成层)
平滑滚动 Lenis / Locomotive Scroll 几乎所有 SOTD 站点 低(RAF 插值)
视差滚动 GSAP ScrollTrigger scrub 分层背景、纵深暗示 低(位移走合成层)

六、性能预算:获奖网站的真实指标

“炫酷”和“卡顿”只有一线之隔。我统计了 47 个 SOTD 站点在 Lighthouse(移动端模拟)上的核心性能指标分布:

性能指标 P50 中位数 P90 SOTD 达标线
Lighthouse Score 72 91 ≥ 65
LCP (s) 2.4 4.1 ≤ 4.0
TBT (ms) 180 620 ≤ 500
CLS 0.02 0.08 ≤ 0.1
TTFB (ms) 320 680 ≤ 600

可以看到,SOTD 并不追求“满分性能”——Lighthouse 中位数只有 72 分,因为后处理和物理引擎必然拖累 TBT。但它们都守住了一条底线:LCP 不超过 4 秒、CLS 不超过 0.1。也就是说,首屏内容必须快速可见,且布局不能跳动,至于后续的交互卡顿,用户对“沉浸式体验”有更高的容忍度。

关键策略

获奖站点普遍采用“首屏轻量、下滚加载重武器”的策略:首屏只用 CSS 动画和 SplitText(低开销),把 Three.js、物理引擎等重资源延迟到用户滚动到对应 section 时再初始化。这能把 LCP 压在 2.5 秒以内,同时不牺牲后续的视觉表现。

七、可复用的技术清单

以下是我在拆解过程中筛选出的最值得学习的开源库与工具,按优先级排列:

不要为了用而用。一个只需要展示产品参数的官网,强行上 Three.js 后处理反而会让 LCP 爆表。先明确“这个效果是否服务于叙事”,再决定技术选型。

八、总结:好设计的本质

拆完这 47 个站点,最大的感受是:获奖网站从来不是“技术堆砌”,而是“技术隐身”。用户不会意识到 ScrollTrigger 在 pin 元素、不会察觉 EffectComposer 在做色散、不会知道 Matter.js 在算碰撞——他们只觉得“这网站好顺、好高级、好想看完”。

真正区分 SOTD 与普通网站的不是用了多新的库,而是每一个技术决策都服务于一个统一的叙事意图。滚动叙事是为了让用户掌控节奏,后处理是为了营造质感,物理交互是为了建立“可触碰”的真实感,字体动效是为了让文字本身成为视觉主角。当这四类模式在同一个设计语言下协同工作时,网站就不再是信息的容器,而是一段可以被体验的旅程。

技术会迭代,库会更替,但“用恰到好处的工程实现恰好够用的创意”这条原则不会变。这或许就是 Awwwards 十年来始终在奖励的东西。