360 前端星 Day4

8 前端工程化

从后往前,没来得及看,肝不动了

9 动画

JavaScript 动画的基本原理

  1. 定时器改变对象属性
  2. 根据新的属性重新渲染动画
function update(ctx) {
	// 更新属性
}
const ticker = new Ticker();
ticker.tick(update, context);

动画的种类

  1. JavaScript 动画
    1. 操作 DOM
    2. Canvas
  2. CSS 动画
    1. transition
    2. animation
  3. SVG 动画
    1. 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>

贝塞尔曲线

贝塞尔曲线概念
B ( p ) = P 0 ( 1 p ) 3 + 3 P 1 p ( 1 p ) 2 + 3 P 2 p 2 ( 1 p ) + P 3 p 3 B(p) = P_0\cdot(1-p)^3+3\cdot P_1\cdot p\cdot(1-p)^2+3\cdot P_2\cdot p^2\cdot(1-p)+P_3\cdot p^3
在这里插入图片描述

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

实战篇

浏览器渲染场景

查看层级 CSS Triggers

  1. JS/CSS > 计算样式 > 布局 > 绘制 > 渲染层合并
  2. JS/CSS > 计算样式 > 绘制 > 渲染层合并
  3. JS/CSS > 计算样式 > 渲染层合并

浏览器渲染流程

  • JavaScript(实现动画、操作 DOM等)
  • Style(Render Tree)
  • Layout(盒模型,确切的位置和大小)
  • Paint(栅格化)
  • Composite(渲染层合并)

JavaScript 改变了 DOM,Style 结合 CSSOM 和 DOM 计算样式产出渲染树,Layout 产出盒模型,Paint 绘制栅格化

不同的 CSS 属性影响的层级不同

通过 Chrome 控制台查看性能表现

在这里插入图片描述

优化步骤

  • 加载
    • 资源效率优化
    • 图片优化
    • 字体优化
    • 关键渲染路径优化
  • 渲染
    • JavaScript 执行优化
    • 避免大型复杂的布局
    • 渲染层合并
发布了10 篇原创文章 · 获赞 8 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/pznavbypte/article/details/105461330