8 前端工程化
从后往前,没来得及看,肝不动了
9 动画
JavaScript 动画的基本原理
- 定时器改变对象属性
- 根据新的属性重新渲染动画
function update(ctx) {
// 更新属性
}
const ticker = new Ticker();
ticker.tick(update, context);
动画的种类
- JavaScript 动画
- 操作 DOM
- Canvas
- CSS 动画
- transition
- animation
- SVG 动画
- SMIL
JavaScript 动画的优缺点
优点:
- 灵活性
- 可控性
- 性能(操作的好的话可以有好的性能)
缺点:
- 易用性(不如 CSS 动画易用)
例子
一个简单的旋转动画(增量实现)
let rotation = 0;
requestAnimationFrame(function update(){
block.style.transform = `rotate(${rotation++})`;
requestAnimationFrame(update);
})
缺点:精确控制难,通过增量来改变速率,增量方式有局限
改进(时间进度控制)
let rotation = 0;
let startTime = null;
const T = 2000;
requestAnimationFrame(function update(){
if(!startTime) startTime = Date.now();
const p = (Date.now() - startTime)/T;
block.style.transform = `rotate(${360 * p}deg)`;
requestAnimationFrame(update);
})
通用版本
// target 是一个 DOM 元素
function update({target}, count) {
target.style.transform = `rotate(${count++}deg)`;
}
class Ticker {
tick(update, context) {
let count = 0;
requestAnimationFrame(function next(){
if(update(context, ++count) !== false){
requestAnimationFrame(next);
}
});
}
}
const ticker = new Ticker();
ticker.tick(update, {target: block});
Timing
class Timing {
constructor({duration, easing} = {}){
this.startTime = Date.now();
this.duration = duration;
this.easing = easing || function(p) {return p};
}
get time() {
return Date.now() - this.startTime;
}
get p() {
return this.easing(Math.min(this.time / this.duration, 1.0));
}
}
class Ticker {
tick(update, context, timing) {
let count = 0;
timing = new Timing(timing);
requestAnimationFrame(function next(){
count++;
if(update(context, {count, timing}) !== false){
requestAnimationFrame(next);
}
})
}
}
运动实现
匀速运动
function update({target}, {timing}) {
target.style.transform = `translate(${200 * timing.p}px, 0)`;
}
const ticker = new Ticker();
ticker.tick(update,
{target: block},
{duration: 2000}
);
自由落体运动
function update({target}, {timing}) {
target.style.transform = `translate(0, ${200 * timing.p}px)`;
}
const ticker = new Ticker();
ticker.tick(update, {target: block}, {
duration: 2000,
easing: p => p ** 2,
});
平抛运动
class Timing {
constructor({duration, easing} = {}) {
this.startTime = Date.now();
this.duration = duration;
this.easing = easing || function(p){return p};
}
get time() {
return Date.now() - this.startTime;
}
get op() {
return Math.min(this.time / this.duration, 1.0);
}
get p() {
return this.easing(this.op);
}
}
function update({target}, {timing}) {
target.style.transform =
`translate(${200 * timing.op}px, ${200 * timing.p}px)`;
}
周期运动
class Timing {
constructor({duration, easing, iterations = 1} = {}) {
this.startTime = Date.now();
this.duration = duration;
this.easing = easing || function(p){return p};
this.iterations = iterations;
}
get time() {
return Date.now() - this.startTime;
}
get finished() {
return this.time / this.duration >= 1.0 * this.iterations;
}
get op() {
let op = Math.min(this.time / this.duration, 1.0 * this.iterations);
if(op < 1.0) return op;
op -= Math.floor(op);
return op > 0 ? op : 1.0;
}
get p() {
return this.easing(this.op);
}
}
椭圆周期
function update({target}, {timing}) {
const x = 150 * Math.cos(Math.PI * 2 * timing.p);
const y = 100 * Math.sin(Math.PI * 2 * timing.p);
target.style.transform = `
translate(${x}px, ${y}px)
`;
}
const ticker = new Ticker();
ticker.tick(update, {target: block},
{duration: 2000, iterations: 10});
连续运动
class Ticker {
tick(update, context, timing) {
let count = 0;
timing = new Timing(timing);
return new Promise((resolve) => {
requestAnimationFrame(function next() {
count++;
if(update(context, {count, timing}) !== false && !timing.finished) {
requestAnimationFrame(next);
} else {
resolve(timing);
}
});
});
}
}
弹跳小球
const down = lerp(setValue, {top: 100}, {top: 300});
const up = lerp(setValue, {top: 300}, {top: 100});
(async function() {
const ticker = new Ticker();
// noprotect
while(1) {
await ticker.tick(down, {target: block},
{duration: 2000, easing: p => p * p});
await ticker.tick(up, {target: block},
{duration: 2000, easing: p => p * (2 - p)});
}
})();
滚动
const roll = lerp((target, {left, rotate}) => {
target.style.left = `${left}px`;
target.style.transform = `rotate(${rotate}deg)`;
},
{left: 100, rotate: 0},
{left: 414, rotate: 720});
const ticker = new Ticker();
ticker.tick(roll, {target: block},
{duration: 2000, easing: p => p});
平稳变速
function forward(target, {y}) {
target.style.top = `${y}px`;
}
(async function() {
const ticker = new Ticker();
await ticker.tick(
lerp(forward, {y: 100}, {y: 200}),
{target: block},
{duration: 2000, easing: p => p * p});
await ticker.tick(
lerp(forward, {y: 200}, {y: 300}),
{target: block},
{duration: 1000, easing: p => p});
await ticker.tick(
lerp(forward, {y: 300}, {y: 350}),
{target: block},
{duration: 1000, easing: p => p * (2 - p)});
}());
甩球
function circle({target}, {timing}) {
const p = timing.p;
const rad = Math.PI * 2 * p;
const x = 200 + 100 * Math.cos(rad);
const y = 200 + 100 * Math.sin(rad);
target.style.left = `${x}px`;
target.style.top = `${y}px`;
}
function shoot({target}, {timing}) {
const p = timing.p;
const rad = Math.PI * 0.2;
const startX = 200 + 100 * Math.cos(rad);
const startY = 200 + 100 * Math.sin(rad);
const vX = -100 * Math.PI * 2 * Math.sin(rad);
const vY = 100 * Math.PI * 2 * Math.cos(rad);
const x = startX + vX * p;
const y = startY + vY * p;
target.style.left = `${x}px`;
target.style.top = `${y}px`;
}
(async function() {
const ticker = new Ticker();
await ticker.tick(circle, {target: block},
{duration: 2000, easing: p => p, iterations: 2.1});
await ticker.tick(shoot, {target: block},
{duration: 2000});
}());
逐帧动画
<style type="text/css">
.sprite {
display:inline-block;
overflow:hidden;
background-repeat: no-repeat;
background-image:url(https://p.ssl.qhimg.com/t01f265b6b6479fffc4.png);
}
.bird0 {width:86px; height:60px; background-position: -178px -2px}
.bird1 {width:86px; height:60px; background-position: -90px -2px}
.bird2 {width:86px; height:60px; background-position: -2px -2px}
#bird{
position: absolute;
left: 100px;
top: 100px;
zoom: 0.5;
}
</style>
<div id="bird" class="sprite bird1"></div>
<script type="text/javascript">
var i = 0;
setInterval(function(){
bird.className = "sprite " + 'bird' + ((i++) % 3);
}, 1000/10);
</script>
Web Animation API(Working Draft)
element.animate(keyframes, options);
target.animate([
{backgroundColor: '#00f', width: '100px', height: '100px', borderRadius: '0'},
{backgroundColor: '#0a0', width: '200px', height: '100px', borderRadius: '0'},
{backgroundColor: '#f0f', width: '200px', height: '200px', borderRadius: '100px'},
], {
duration: 5000,
fill: 'forwards',
});
逐帧动画
<style type="text/css">
.sprite {
display:inline-block;
overflow:hidden;
background-repeat: no-repeat;
background-image:url(https://p.ssl.qhimg.com/t01f265b6b6479fffc4.png);
}
.bird0 {width:86px; height:60px; background-position: -178px -2px}
.bird1 {width:86px; height:60px; background-position: -90px -2px}
.bird2 {width:86px; height:60px; background-position: -2px -2px}
#bird{
position: absolute;
left: 100px;
top: 100px;
zoom: 0.5;
}
</style>
<div id="bird" class="sprite bird1"></div>
<script type="text/javascript">
var i = 0;
setInterval(function(){
bird.className = "sprite " + 'bird' + ((i++) % 3);
}, 1000/10);
</script>
贝塞尔曲线
function bezierPath(x1, y1, x2, y2, p) {
const x = 3 * x1 * p * (1 - p) ** 2 + 3 * x2 * p ** 2 * (1 - p) + p ** 3;
const y = 3 * y1 * p * (1 - p) ** 2 + 3 * y2 * p ** 2 * (1 - p) + p ** 3;
return [x, y];
}
function update({target}, {timing}) {
const [px, py] = bezierPath(0.2, 0.6, 0.8, 0.2, timing.p);
target.style.transform = `translate(${100 * px}px, ${100 * py}px)`;
}
const ticker = new Ticker();
ticker.tick(update, {target: block}, {
duration: 2000,
easing: p => p * (2 - p),
});
有现成的 bezier-easing 库可以使用,使时间轴是贝塞尔曲线
bezier-easing
function update({target}, {timing}) {
target.style.transform = `translate(${100 * timing.p}px, 0)`;
}
const ticker = new Ticker();
ticker.tick(update, {target: block}, {
duration: 2000,
easing: BezierEasing(0.5, -1.5, 0.5, 2.5),
});
Web Animation API
function animate(target, keyframes, options) {
const anim = target.animate(keyframes, options);
return new Promise((resolve) => {
anim.onfinish = function() {
resolve(anim);
}
});
}
(async function() {
await animate(ball1, [
{top: '10px'},
{top: '150px'},
], {
duration: 2000,
easing: 'ease-in-out',
fill: 'forwards',
});
await animate(ball2, [
{top: '200px'},
{top: '350px'},
], {
duration: 2000,
easing: 'ease-in-out',
fill: 'forwards',
});
await animate(ball3, [
{top: '400px'},
{top: '550px'},
], {
duration: 2000,
easing: 'ease-in-out',
fill: 'forwards',
});
}());
10 性能优化
前端性能与用户体验息息相关,前端优化对于改善用户体验有很大意义
RAIL 模型
RAIL 是以用户为中心的性能模型,每个网络应用都具有与其生命周期有关的四个方面,而且这些方面以不同的方式影响着性能。
延迟与用户反映
0-16ms | 用户可以感知每秒渲染 60 帧的平滑动画转场。每帧 16ms,留给应用大约 10ms 的时间来生成一帧 |
---|---|
0-100ms | 在此时间窗口内响应用户操作,他们会觉得可以立即获得结果。时间再长,操作与反应之间的连接就会中断 |
100-300ms | 轻微可察觉的延迟 |
300-1000ms | 延迟感觉像是任务自然和持续发展的一部分(用户觉得这是正常流,但不会觉得快) |
1000+ms(>1s) | 用户注意力将离开他们正在执行的任务 |
10000+ms(>10s) | 用户感到失望,可能会放弃任务;之后他们或许不会再回来 |
移动端与 PC 的差异,人们对于移动端的白屏容忍度比 PC 端高,移动端 5s 之内的白屏都可以被宽容。
响应:50ms 处理事件
目标:在 100ms 内响应用户输入
指导:
- 50ms 内处理用户输入事件,确保 100ms 内反馈用户可视的响应
- 对于开销大的任务可分隔任务处理,或放到 worker 进程中执行,避免影响到用户交互
- 处理时间超过 50ms 的操作,始终给予反馈(进度和活动指示器)
处理任务的间隔如果为 50ms,事件的排队时间或接近 50ms,所以需要在 50ms 处理完毕事件
动画:10ms 一帧
目标:
- 10ms 或更短的时间内生成一帧
- 10ms = (1000ms / 60f 约等于 16.66ms) - 6ms(每帧开销 6ms,渲染帧的预算)
- 视觉平滑
指导
- 动画中尽量不要处理逻辑,提高达到 60fps 的机会
- 动画类型
- 滚动。包括甩动,及用户开始滚动,然后开放,页面继续滚动
- 视觉动画。包括入口和出口、补间和加载指示器
- 拖拽动画通常伴随用户交互。包括地图平移和缩放
空闲时间最大化
目标:
- 最大化空闲时间以增加页面在 100ms 内响应用户输入的几率
指导:
- 预加载数据,利用空闲时间完成推迟的工作
- 空闲时间期间用户交互优先级最高
加载:5s 内呈现交互内容
目标:
- 首屏加载连接 3G-Slow 的中档移动设备 5s 内呈现可交互内容
- 非首屏加载应该在 2s 内完成
指导:
- 测试用户常用设备和网络连接情况的性能
- 优化关键渲染路径以解除阻止渲染
- 启用渐进式渲染和在后台执行一些工作
- 影响加载性能的因素:
- 网络速度
- 硬件
- JavaScript 解析
关键指标
- 响应:100ms 内响应用户输入
- 动画:动画或滚动时,10ms 产生一帧
- 空闲时间:主线程空闲时间最大化
- 加载:在 1000ms 内呈现交互内容
- 以用户为中心
评估工具
- Lighthouse
- WebPageTest
- Chrome DevTools
实战篇
浏览器渲染场景
- JS/CSS > 计算样式 > 布局 > 绘制 > 渲染层合并
- JS/CSS > 计算样式 > 绘制 > 渲染层合并
- JS/CSS > 计算样式 > 渲染层合并
浏览器渲染流程
- JavaScript(实现动画、操作 DOM等)
- Style(Render Tree)
- Layout(盒模型,确切的位置和大小)
- Paint(栅格化)
- Composite(渲染层合并)
JavaScript 改变了 DOM,Style 结合 CSSOM 和 DOM 计算样式产出渲染树,Layout 产出盒模型,Paint 绘制栅格化
不同的 CSS 属性影响的层级不同
通过 Chrome 控制台查看性能表现
优化步骤
- 加载
- 资源效率优化
- 图片优化
- 字体优化
- 关键渲染路径优化
- 渲染
- JavaScript 执行优化
- 避免大型复杂的布局
- 渲染层合并