前端于我
前端 / 性能 / 优化

前端性能优化总结

“少年,体验过健步如飞的感觉吗?” – 搬砖人

首屏首次加载优化

为什么叫首屏首次加载优化呢,是因为首次的时候没有缓存,这时候最考验产品的优化程度,虽然加载很大程度上取决于用户的网络状态,但是显然这并不能说服老板。

分包加载

提到首屏加载优化,最先想到的应该还是分包加载。分包加载指的就是通过代码拆分的手段,将首屏代码资源单独分出一个代码包(js文件),当然,大部分时候还可能会拆出一个公用包用来承载一些公用库(如react.js / axios.js等)。

而分包的手段在日常工作中已经是十分常见了,不论是webpack分包,小程序分包,亦或是小游戏分包。目前都已经提供了比较成熟的解决方案。

这里简单介绍一下这几种分包方式的用法:

webpack分包

const path = require('path');
const webpack = require('webpack');
const HTMLWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    index: './src/index.js',
    another: './src/another-module.js'
  },
  plugins: [
    new HTMLWebpackPlugin({
      title: 'Code Splitting'
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'common' // 指定公共 bundle 的名称。
    })
  ],
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};

代码如上,该webpack指定了两个entry,并且使用CommonsChunkPlugin插件将公用模块拆成一个独立的代码包,最终会生成index.bundle.jsanother.bundle.jscommon.bundle.js

而在加载的时候只会加载指定的entry包以及公共包,如果在业务代码比较庞大的时候,将非首页的,非必要的,非常用的代码单独拆分成一个或几个包就非常的必要了。

ps: 还有一种方式是,不需要指定多个entry,而是使用import()动态加载功能

小程序(以微信小程序为例)分包

小程序内分包又可以有两种形式,第一种是常规的分包,即分为主包以及多个小包。第二种则是根据独立功能拆分出独立包。

首先是常规分包,只需要在app.json内设置subpackages字段,在打包是构建工具就会将代码按照配置打包成多个包。

注意的是配置是按照页面路径来区分分包逻辑的,那么此时如果多个子包应用了同一模块,那么该模块将会被打入主包,这样才能够被多个子包复用。

所以打包逻辑一般遵循“首页(或者几个访问量大的)页面单独一个包,其他页面则根据功能是否递进或耦合进行拆分”的规则。

{
  "pages":[
    "pages/index",
    "pages/logs"
  ],
  "subpackages": [
    {
      "root": "packageA",
      "pages": [
        "pages/cat",
        "pages/dog"
      ]
    }, {
      "root": "packageB",
      "name": "pack2",
      "pages": [
        "pages/apple",
        "pages/banana"
      ]
    }
  ]
}

第二种形式是拆分独立包,这种模式一般运用在某个访问量大,独立且功能单一的页面。

比如:某某火车票抢票小程序的分享好友加速,某某生鲜小程序的分享红包页面等。

可以注意到应用独立包的场景都是符合“访问量大,独立,功能单一”的场景,因为这些场景不依赖其他逻辑,一般仅仅是拉回流用户,引导用户进入小程序消费,所以需要做到快速响应,先让用户进来,再通过红包/折扣的诱惑引导用户进一步消费。

这时候就非常考验该页面的加载性能,一般这种场景都是用户点击分享卡片进入,目标性较差,如遇到加载时间较长很容易失去耐心直接退出。而独立包则十分完美的满足了这种场景需求。

配置方面,仅需将该分包的independent设为true就可以了,但是需要注意的是,该页面不要包含大的模块,否则打出来的包大了,就起不到”小而精“的作用了。且独立包与其他包是独立的,也就是说就算主包与独立包引用了同一模块,那独立报与主包都会包含该模块。

{
  "pages": [
    "pages/index",
    "pages/logs"
  ],
  "subpackages": [
    {
      "root": "moduleA",
      "pages": [
        "pages/rabbit",
        "pages/squirrel"
      ]
    }, {
      "root": "moduleB",
      "pages": [
        "pages/pear",
        "pages/pineapple"
      ],
      "independent": true
    }
  ]
}

小游戏分包(以微信小游戏 && cocos creator为例)

微信小游戏也支持分包加载的配置,配置方式跟小程序配置是一样的,只需要配置一下game.jsonsubpackages字段就可以了。

{
  ...
  "subpackages": [
    {
      "name": "stage1",
      "root": "stage1/" // 可以指定一个目录,目录根目录下的 game.js 会作为入口文件,目录下所有资源将会统一打包
    }, {
      "name": "stage2",
      "root": "stage2.js" // 也可以指定一个 JS 文件
    }
  ]
  ...
}

而加载则使用wx.loadSubpackage()api来触发分包的下载。

而cocos creator也在v2.4.0版本支持了分包的配置
配置的方式是在编辑内根据文件夹配置来分包。

分包配置示例

加载则是使用cc.assetManager.loadBundle()api来控制分包的加载。

很不幸的是,v2.4.0刚出不到一个月,暂时还没上手尝试,不过按照惯例,估计坑不少。

以上就是代码分包的几个场景了,往往代码分包产生的效益是立竿见影的,这也是为什么它频繁被提起的原因,首次加载的资源少了,加载速度自然就上来了。

服务端渲染

服务端渲染主要解决的是两个问题

  1. 首屏渲染加速

  2. SEO优化

服务端渲染主要是在服务端提前渲染好dom,然后浏览器在文档返回之后不需要等待与运行js即可立即在页面中呈现出内容,而搜索引擎也可以通过检索文档内容,提高页面被呈现的几率(客户端渲染则不行,因为文档是空的)。

但是服务端渲染会将原本由用户承担的渲染成本集中到服务器上,所以在成本与效益之间如何均衡就要看各自业务的考量了。

这里有一篇react服务端渲染项目搭建, 感兴趣的可以看看。

图片优化

通常一个互联网项目少不了图片的出现,web最初也只是用来承载文本与图片,只是在时代发展下逐渐扩充了其功能,才形成目前五花八门的应用场景(视频/音频等)。

而归根结底,文本与图片的应用也是重中之重。众所周知,图片的质量越大,加载则越慢。应该接触过互联网的人都有经历过网络慢的时候,打开一个网页肯定是文字先出来,然后才是图片。

这是因为文本小且它是跟文档一起被加载的,而图片则是在文档被解析中或解析后才发出的请求。在本身加载就比较靠后的情况下,如果图片还大,则会出现加载很久甚至加载失败的情况,或许这种状况在你看来正常,但是在用户眼中,就是优化不当或者是个bug。

由于图片优化内容比较多,所以单独拎出来作为一块,请移步图片优化实践

使用CDN

关于cdn的文章五花八门,没看过的赶紧百度一下。简单来说CDN就是在世界各地部署服务器,当你需要一个静态资源的时候,就可以直接从离你最近的那台服务器上获取。

比如人在北京,但是你的服务器在纽约,那你要获取个资源就得跑一个地球周长那么远(一来一回)。但是如果你把静态资源部署到北京的cdn服务器上,那就是两步路的事情,cdn就是那么牛逼。

而一般cdn服务器的地址跟业务服务器地址是分开的,这间接的帮你把资源与api的域名区分开,这样做有两个好处:

  1. 请求资源不会携带cookie。(统一域名下的所有请求都会携带cookie)

  2. 请求并发数增加(Chrome的一个tab中同域名下最多并发6条请求,多的会排队)

所以能用cdn就用上,反正对于前端来说也就是一键部署的事儿。

合并散碎小文件,拆分资源域名,延迟加载大文件

前面多处地方提到浏览器有并发数限制,而一般一个网站一打开就会有十几二十条以上的请求被发出,此时就很大可能会触发请求数上限的限制了。

而如果你在页面中加载了多个大文件,那就很可能请求数被这些大文件占用从而导致其他资源请求无法发出。一般来说,”js + css + 大图“再加一些音视频,很容易就可以超过6个大文件。

这时候就需要将暂时不会使用到的大文件延后加载了,前面提到的代码分包也是这个目的,比如加载大图或者音视频,完全可以放到DOMContentLoaded之后再加载,优先保证js跑起来,页面能有展示内容。

而合并散碎小文件也很好理解,比如你有三个css分别是base.css, theme.css, page.css。这时候合并成一个反而更好,即减少了网络请求,又不需要考虑其中某个css加载失败的异常(要死一起死)。

同理还有小图标的合并,js文件的合并等。

拆分资源域名 是最后的优化手段,毕竟不可能那么多域名给你用,公司资源都是宝贵的,但是真当你把资源都合并之后还是有这么多请求,那就要认真考虑一下是否拆分域名了(这种情况我在小游戏中遇到过)。

特殊的优化手段(离线包与预渲染、模版页)

页面的渲染必然依赖各种资源,而如果我们可以提前加载好那些资源是不是理论上可以做到快速启动一个页面呢?

但是肯定的是,单纯借用web与浏览器的能力是无法在用户没有访问过页面之前就拥有页面的资源缓存的。

可是现在流行的 混合开发(Hybrid App) 却可以提供这种能力。

离线包

在原生客户端,可以为web定制一种提前获得页面资源的手段,在应用打包时内置一份前端项目代码,在webview发出请求的时候对请求进行拦截匹配,当匹配成功时则直接使用离线资源,这就是简单的 离线包 的概念。

要知道当你使用离线资源的速度是很快的,这能给一个web应用的体验带来极大的提升。

但是这样做也有弊端,因为web项目一般都具备快速迭代的特性,如果内置到应用包里面,当页面需要更新怎么办?

这时我们就需要提供一份配置去控制我们的离线包是否过期了。

假设我们提供了一个json文件,文件内包含离线包版本号,离线资源匹配规则,离线包下载地址等信息,而当应用启动时去服务器上请求最新的离线包配置与本地配置进行对比,如果版本不一致则覆盖本地缓存,这就完美解决了离线包版本控制的问题了。

但是需要注意的是,主文档不应该被作为离线资源下发,因为当业务更改时,打包出来的js与css的hash变化只有在html文档中才有引用,所以如果html文档也使用离线包,在离线包还未更新的情况下,主文档就一直引用老版本资源,而在线请求的主文档则没有这个问题,即使没有命中离线资源,它也还是可以请求在线资源,并不影响用户的访问。

既然离线包已经那么强大了,还有没有进一步优化空间呢?

离线包只是加载静态资源很快而已,但是它还需要初始化 webview的耗时,还需要加载主文档与发起请求。

这些步骤我们也可以想办法去进行优化,尽量减少这些耗时。这就涉及到另一个技术 – “预渲染”

预渲染

预渲染指的是当你还未在一个应用内访问web模块时,应用已经提前渲染后该web服务,只需要用户点击时调出该webview,即可立即呈现内容。

听起来似乎很简单,但是这其中还是有几个难点。

这就涉及到数据分析了,分析用户日常最经常使用哪个模块,以及用户目前进入路径是否有极强目的性的。对于新用户则可以通过判断来源,依靠大部分用户的选择来决定提前渲染哪个服务。

这点很重要,一般的产品会在页面的加载前,加载成功后进行数据打点来验证访问漏斗等数据。如果这时候不进行区分,则这块的数据就不准确,并且一些逻辑也需要定制,比如进入页面自动播放,展示一个3s的toast等。这可以让客户端提供定制的jsBridge api来控制。

对于资源的损耗,预渲染是无法保证用户会进入渲染的页面的,所以也就导致浪费了用户流量与加大了服务器压力,最后用户却并没有买单,甚至看到你的应用这么吃流量怒而删app走人。暂时看来这是无解的,只能从第一点的角度入手尽量保证用户进行消费。

模版页

模版页与预渲染类似,只不过模版页应该是一个纯静态的页面,只有它被展示在前台才会去请求数据。

所以模版页只包含整个页面的所有资源,但是不包括数据,这样做有一个好处就是当页面中有多个页面入口时,此时预渲染只能渲染一个webview,但是当你不确定用户会进入哪个页面时,你可以选择加载并渲染页面的基础框架(只要这些页面用的框架都是同一套框架)。

这时候不论用户点击哪个页面都能快速相应,然后直接从native端透传数据或者直接发起请求,从而减少了“初始化webview,建立链接,解析并渲染,加载静态资源”这些工作的耗时,直接调出渲染好的webview(如果有骨架屏就更好了)。

需要注意的是[ 离线包, 预渲染, 模版页] 都是基于有一个原生客户端给你兜底的情况,一般的web应用只能通过运用一些缓存的手段达到拥有缓存后大大加快访问速度。往往最优的做法是将这些手段组合起来,就能达到睥睨原生应用的秒开体验。

或许有人会说我只是打开个页面而已,并不需要搞的那么麻烦吧。其实不然,任何公司做一个产品都不会秉持着能用就好的心态,体验当然是越优秀越好,而且这工作并不是特异性很强的业务功能,而是通用的基建模块,只要把架子打好了,之后就可以一劳永逸的享受这份优异丝滑的体验。

最后的倔强(骨架屏或loading动画)

如果什么手段都用了,但是老板还是喜欢清掉缓存在封闭的厕所看自己的产品的话,那你也没辙。

此时你唯一要想的就是如何让老板(用户)明白,不是你的程序出bug了,是他网络不好。

做过vue/react的应该都遇到过关键js代码加载时卡住了,导致页面一片空白,这就是“你的程序出bug了“的日常套路。

而这时候给页面一个骨架屏,或者一个loading动画,提示用户现在正在加载中,而不是出现了bug,会让用户更加有耐心等待下去。

非首屏加载优化

合理利用浏览器缓存

浏览器自己就拥有一套缓存,跟前面说过的离线包很类似,它可以把静态资源缓存到内存或磁盘,下次再访问的时候直接使用缓存资源,快到飞起。

浏览器缓存有以下几种:

  1. http Cache(disk Cache)

  2. Memory Cache

  3. Service Worker Cache

  4. Push Cache

它又分为 强缓存协商缓存 ,它的说明请移步Http缓存机制

但是在内存中的缓存在页面关闭时,该内存被释放,缓存也就没了。而且因为内存空间有限,它只会缓存一些小文件。

Service Worker 是一种独立于主线程之外的 Javascript 线程。它脱离于浏览器窗体,因此无法直接访问 DOM。这样独立的个性使得 Service Worker 的“个人行为”无法干扰页面的性能,这个“幕后工作者”可以帮我们实现离线缓存、消息推送和网络代理等功能。我们借助 Service worker 实现的离线缓存就称为 Service Worker Cache。

合理运用这些缓存,尽量不要频繁的更新代码,因为更新代码之后hash变了,就导致之前的缓存用不了。

当然,如果每次更新都是一些小功能话,应该也只是单纯影响该功能的js包,其他的绝大多数缓存还是有效的(再次说明了分包的重要性)。

懒加载

懒加载为什么会放在非首屏呢,因为首屏资源基本上只需要保证展示在一屏内的资源快速加载出来就可以了,所以我把它归类到非首屏了。

懒加载在图片优化也有简单的提到,这里来说以下它的具体实现。

一般现在的项目都是直接使用IntersectionObserver来实现元素的曝光与显示,真是简单有好用。(ie不兼容,但是我已经抛弃ie了)

直接上手写一个React的Image组件

// 直接使用一个IntersectionObserver实例
const intersectionObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.intersectionRatio > 0) { // 判断是否进入检测区域(注意这里是检测区域不是可视区域)
      const targetImg = entry.target;
      if (!targetImg.src) { // 如果进入的图片元素没有src属性,则给它赋值
        const src = targetImg.getAttribute('data-src');
        if (src) {
          targetImg.src = src;
          intersectionObserver.unobserve(targetImg); // 赋值完成之后把这个已经加载资源的元素从检测列表中移除
        }
      }
    }
  });
}, {
  rootMargin: '50% 50% 50% 50%', // 定义检测区域大小,这里分别是距离屏幕四边半个屏幕,这么做的原因是因为不能进入视图才加载,会来不及,所以得提前半个屏幕加载
});

export default class Image extends Component {
  ref = React.createRef();

  componentDidMount() {
    const { current } = this.ref;
    intersectionObserver.observe(current); // 将该img加入检测
  }

  componentWillUnmount() {
    const { current } = this.ref;
    if (!current.src) {
      intersectionObserver.unobserve(current); // 如果移除时还没进入视图,则移除检测
    }
  }

  render() {
    return <img className="lazy-image" data-src={this.props.src} alt="" ref={this.ref} />;
  }
}

这是一个图片监听的简单实现,可以扩展一下,加个是否需要懒加载的控制,或者加class,style的控制,加一些回调函数等。

同时这不仅可以作为懒加载的手段,也可以修改一下作为一个曝光打点的组件。关键在个人怎么使用。

这里需要注意一下IntersectionObserver只有在浏览器空闲的时候才会执行,如果你让浏览器忙起来了,那可能这个事件的触发频率就很低,或者可以使用getBoundingClientRectAPI自己封装一个曝光监听函数,但是这样做会使你的浏览器更"忙"。

减少DOM交互

与DOM交互的性能损耗是很大的,所以我们应该尽量减少js与dom的交互。尽量将重复使用的dom变量缓存起来,不要每次都去获取。

长列表优化

很多产品都拥有长列表,比如电商,比如论坛。一般这种流式的内容都会有上拉加载的功能,而当你上拉次数足够多,你的dom数就会达到一个庞大的量。

当列表项达到1000+时,或许你就会明显的感觉到你的页面开始变得卡顿了。此时就要采取策略去优化长列表了。

目前前端层面对于长列表的优化差强人意,基本上的做法都是通过getBoundingClientRect()计算或者IntersectionObserverapi判断元素是否曝光,或者处于即将曝光的状态,从而决定是否渲染列表项。

但是这样会产生监听或计算大量节点,与dom的交互频繁导致优化效果不理想。

我写了一篇[长列表优化方案][https://limaosheng.top/article/5a9bdbdb0fc389b01f1a840d9a47600d-e6ebc01bd24898098c8acd6f4b4e73f3-1.html],里面详细解释了通过js计算节点高度做的长列表优化手段。

使用👉react-infinite-auto-scroller

利用Resource Hints进行资源的预加载

Respirce Hints规范定义了如何使用<link>标签的dns-prefetch, preconnect, prefetch, prerender属性控制资源的加载。(dns预解析/预链接/预获取/预渲染 还有 预加载preload)

Resource Hints 使用简单,兼容性好,且不会产生副作用。何乐而不为呢。

详细介绍点击 👉Resource Hints介绍

组件渲染异常兜底

在日常开发中,大多数人应该遇见过“一个数据结构错误导致页面空白”的问题,往往这种锅是可以丢给后端同事的,但是并不是说前端毫无责任,因为页面空白是可以避免的,只是你没做而已。

let DefaultErrorComponent = null;

export function setDefaultErrorComponent(ErrorComponent) {
  DefaultErrorComponent = ErrorComponent;
}

export default function createCatchError(Component, ErrorComponent) {
  return class CatchErrore extends React.Component {
    state = {
      hasError: false,
    };

    static getDerivedStateFromError(error) { // getDerivedStateFromError与componentDidCatch都可以用来捕获组件渲染产生的错误
      console.log(error);
      return { hasError: true };
    }

    // componentDidCatch() { 这个方法在未来的版本中会被移除
    //   this.setState({ hasError: true });
    // }

    render() {
      if (this.state.hasError) {
        return ErrorComponent ? <ErrorComponent {...this.props} /> : (DefaultErrorComponent ? <DefaultErrorComponent {...this.props}/> : null)
      }
      return <Component {...this.props} />;
    }
  }
}

上面就是一个简易的渲染异常兜底组件,使用高阶函数将需要兜底的组件与异常时显示的组件一同传入,返回一个兜底后的组件,同时提供设置默认兜底组件的能力。

这样遇到后端返回数据错误的时候也能够保证整个页面不会挂掉,而仅仅是数据异常的那一小块内容显示出错误提醒。

运行优化(节流与防抖)

虽说我经常无法将节流与防抖的概念与其名称一一对应起来,但是在日常工作中还是经常会遇到需要使用到节流与防抖的场景。

比如说在监听列表滚动时就经常需要使用到 节流,而在用户输入文本的时候则比较常用 防抖

其细节可以看一下👉 节流防抖的简单实现

如果页面中有类似scroll/resize这类的需求的话,建议还是加上节流或者防抖,因为它会频繁触发。

React中的事件绑定优化

经常在别人写的react中看到类似如下的代码:

  // ...
  eventHandle() {}

  render() {
    return (
      <div>
        {this.state.list.map(item => {
          return <Component eventHandle={() => this.eventHandle()}/>;
          /** 或者这样
          * return <Component eventHandle={this.eventHandle.bind(this)}/>;
          */
        })}
      </div>
    );
  }
  // ...

乍一看是没什么问题,项目跑起来也很完美,但是这里的eventHandle不应该用箭头函数。

如果这里使用的是箭头函数,那么每次render的时候都会循环创建n个函数绑定,这并不必要。通常如果不需要传递列表项的参数,我们只需要将this在声明阶段绑定上去就好了,很简单,将箭头函数挪一下位置。

  // ...
  eventHandle = () => {}

  render() {
    return (
      <div>
        {this.state.list.map((item, index) => {
          return <Component key={index} eventHandle={this.eventHandle}/>;
        })}
      </div>
    );
  }
  // ...

这样我们每次渲染的时候都是复用了一个函数,性能又得到了一丢丢的提升。

但是这种都是没有递归传参的时候才会这么使用,如果需要用到item传参呢?

其实我们可以将参数传入组件中,在组件中触发的时候再将参数传回来,这样同样不需要声明多个函数。

  // ...
  eventHandle = (item) => {}

  render() {
    return (
      <div>
        {this.state.list.map((item, index) => {
          return <Component key={index} item={item} eventHandle={this.eventHandle}/>;
        })}
      </div>
    );
  }
  // ...

或许这时候会有杠精说“你这是react组件才能这么用吧,如果我循环遍历出来的是比较简单的dom结构呢?”。

即使是简单dom结构也是可以优化的。

  // ...
  eventHandle = (event) => {
    var index = +event.target.getAttrivute('data-index');
    var item = this.state.list[index];
    // ...
  }

  render() {
    return (
      <div>
        {this.state.list.map((_, index) => {
          return <div key={index} data-index={index} eventHandle={this.eventHandle}></div>;
        })}
      </div>
    );
  }
  // ...

甚至是使用事件代理

还有一种古老的手段可以完美的解决这个遍历绑定事件的问题,那就是事件代理,使用事件代理可以让你减少对dom的引用,以及减少函数的声明。

事件代理只有一点是需要注意的,那就是当点击需要响应的元素的子元素时,如何判断这个子元素在需要响应元素内。

目前主要有两个方式,1. 使用matches方法,2. 循环遍历。

但我更倾向于两者结合起来使用。

element.addEventListener('click', e => {
  if(e.target.matches('li') || e.target.matches('li *')) {
    var eventElement = e.target;
    while (eventElement && eventElement.getAttribute('data-event') !== 'eventName') {
      if (eventElement === element) {
        eventElement = null;
        break;
      }
      eventElement = eventElement.parentNode;
    }
    // todo ...
  }
})

跳转前请求

经常,我们进入一个页面后,会在该页面组件的componentDidMount中发起数据请求,但是其实在跳转到请求之间存在了一定的空档期,尽管跳转并不是非常耗时,但是当页面是通过动态加载的,此时数据请求的发出时间就要依赖于页面的加载时间。

那么这段时间是不是可以省略的呢?答案很明显,只要在页面跳转时同步发起数据请求,当页面请求&渲染于数据请求并行,那么时间就可以从“n+n”变成"n",如果你的接口响应速度快,说不定页面渲染出来时立即便可以看到完整的数据。

怎么做?由于react的class组件只会响应自身state的变更(或者props的变更)。自身的state只有在组件创建之后才会初始化。所以只能通过第三方状态库来实现了。

例如redux / mobx都可以简单的实现一个页面对应一个store,然后将线上数据都保存在store内,就既可以将请求与组件分离,又可以起到缓存数据的效果。(并且给请求函数做单元测试也更加方便)。

最后,或许react新推出的hooks也可以起到全局状态管理的作用,这块后面再补上相应的文章详细介绍一下吧!

关于如何使用React Hooks简单实现全局状态管理,可以参考另一篇介绍 React使用useReducer实现全局状态管理

使用离线应用技术(pwa)

如果你的应用对于用户来说是一个强依赖性强约束型的应用,那么你可以考虑一下使用service worker将整个应用离线起来。

为什么需要强依赖性强约束性的才推荐用pwa呢,因为pwa更新比较麻烦,可能出现你已经发布了新的版本,但是用户手机上刷了两三遍还是老版本的情况。所以对于迭代速度快的产品最好还是别随便上pwa, 后期管理起缓存状态会比较麻烦。

详细的技术优化点可以查看应用缓存与pwa

密集型计算任务请使用web worker

密集型的计算因为js的单进程的影响会堵塞js的执行,所以最佳实践是将大量的计算任务交给web worker。

css优化

  1. css嵌套规则不要超过3层。(css的匹配规则是从右到左的,也就意味着.a .b .c会优先匹配所有.c的元素,再匹配.b.a,所以最好使用较为明确的类名。

  2. css类名分模块划分好,且常见的类名一定要加父类名约束。

/* bad */
.btn {
  font-size: 24px;
}
/* good */
.notice-component .btn {
  font-size: 24px;
}

总结

对于首屏,我们要控制好资源的大小(代码与图片),安排好资源加载的顺序,利用好cdn,必要时使用服务端渲染是一个很好的选择。

对于非首屏,我们还需要关注缓存状况,关注代码执行效率。

别小看每一个性能优化的小技巧,可能每个小技巧能够提升的性能有限,但是当这些性能累积起来,却能让你的代码高效的运行起来。

其实性能优化(不仅是前端)无非就是几点,“并行(并发),预加载(预渲染),懒加载(分包&图片),复用(实例),缓存(数据/文件),压缩(代码/数据)”,从每一点入手,深入优化,多问问自己还能做什么,性能自然就上去了。

参考文档

前端性能优化原理与实践

长列表优化方案

图片优化实践

Resource Hints介绍

react服务端渲染项目搭建

Http缓存机制

React使用useReducer实现全局状态管理

应用缓存与pwa

发表于: 2020-07-20