一次网站的性能优化之路 -- 天下武功,唯快不破

简介: 一次网站的性能优化之路 -- 天下武功,唯快不破

1. 用户期待的速度体验


2018 年 8 月,百度搜索资源平台发布的《百度移动搜索落地页体验白皮书 4.0 》中提到:页面的首屏内容应在 1.5 秒内加载完成


也许有人有疑惑:为什么是 1.5 秒内?哪些方式可加快加载速度?以下将为您解答这些疑问!


移动互联网时代,用户对于网页的打开速度要求越来越高。百度用户体验部研究表明,页面放弃率和页面的打开时间关系如下图所示:


微信图片_20220513153607.png


根据百度用户体验部的研究结果来看,普通用户期望且能够接受的页面加载时间在 3 秒以内。若页面的加载时间过慢,用户就会失去耐心而选择离开,这对用户和站长来说都是一大损失。


百度搜索资源平台有 “闪电算法” 的支持,为了能够保障用户体验,给予优秀站点更多面向用户的机会,“闪电算法”在 2017 年 10 月初上线。


闪电算法 的具体内容如下:


移动网页首屏在 2 秒之内完成打开的,在移动搜索下将获得提升页面评价优待,获得流量倾斜;同时,在移动搜索页面首屏加载非常慢(3 秒及以上)的网页将会被打压。


2. 分析问题


未优化之前,首屏时间居然大概要 7 - 10 秒,简直不要太闹心。


微信图片_20220513153626.jpg


微信图片_20220513153640.png


开始分析问题,先来看下 network :


微信图片_20220513153714.png


主要问题:


  • 第一个文章列表接口用了 4.42 秒
  • 其他的后端接口速度也不快
  • 另外 js css 等静态的文件也很大,请求的时间也很长


我还用了 Lighthouse 来测试和分析我的网站。


Lighthouse 是一个开源的自动化工具,用于改进网络应用的质量。 你可以将其作为一个 Chrome 扩展程序运行,或从命令行运行。 为 Lighthouse 提供一个需要审查的网址,它将针对此页面运行一连串的测试,然后生成一个有关页面性能的报告。

未优化之前:


微信图片_20220513153734.png


上栏内容分别是页面性能、PWA(渐进式 Web 应用)、可访问性(无障碍)、最佳实践、SEO 五项指标的跑分。


下栏是每一个指标的细化性能评估。


再看下 Lighthouse 对性能问题给出了可行的建议、以及每一项优化操作预期会帮我们节省的时间:


微信图片_20220513153746.png


从上面可以看出,主要问题:


  • 图片太大
  • 一开始图片就加载了太多

知道问题所在就已经成功了一半了,接下来便开始优化之路。


微信图片_20220513153805.jpg


2. 优化之路


网页速度优化的方法实在太多,本文只说本次优化用到的方法。


2.1 前端优化


本项目前端部分是用了 react 和 antd,但是 webpack 用的还是 3.8.X 。


2.1.1 webpack 打包优化


因为 webpack4 对打包做了很多优化,比如 Tree-Shaking ,所以我用最新的 react-create-app 重构了一次项目,把项目升级了一遍,所有的依赖包都是目前最新的稳定版了,webpack 也升级到了 4.28.3 。


用最新 react-create-app 创建的项目,很多配置已经是很好了的,笔者只修改了两处地方。


  1. 打包配置修改了 webpack.config.js 的这一行代码:


// Source maps are resource heavy and can cause out of memory issue for large source files.
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
// 把上面的代码修改为: 
const shouldUseSourceMap = process.env.NODE_ENV === 'production' ? false : true;


生产环境下,打包去掉 SourceMap,静态文件就很小了,从 13M 变成了 3M 。

  1. 还修改了图片打包大小的限制,这样子小于 40K 的图片都会变成 base64 的图片格式。


{
      test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/,/\.jpg$/,/\.svg$/],
      loader: require.resolve('url-loader'),
      options: {
            limit: 40000, // 把默认的 10000 修改为 40000
            name: 'static/media/[name].[hash:8].[ext]',
      },
 }


2.1.2 去掉没用的文件


比如之前可能觉得会有用的文件,后面发现用不到了,注释或者删除,比如 reducers 里面的 home 模块。


import { combineReducers } from 'redux'
import { connectRouter } from 'connected-react-router'
// import { home } from './module/home'
import { user } from './module/user'
import { articles } from './module/articles'
const rootReducer = (history) => combineReducers({
// home, 
  user,
  articles,
  router: connectRouter(history)
})


2.1.3 图片处理


  • 把一些静态文件再用 photoshop 换一种格式或者压缩了一下, 比如 logo 图片,原本 111k,压缩后是 23K。
  • 首页的文章列表图片,修改为懒加载的方式加载。


之前因为不想为了个懒加载功能而引用一个插件,所以想自己实现,看了网上关于图片懒加载的一些代码,再结合本项目,实现了一个图片懒加载功能,加入了 事件的节流(throttle)与防抖(debounce)


代码如下:


// fn 是事件回调, delay 是时间间隔的阈值
function throttle(fn, delay) {
  // last 为上一次触发回调的时间, timer 是定时器
  let last = 0,
    timer = null;
  // 将throttle处理结果当作函数返回
  return function() {
    // 保留调用时的 this 上下文
    let context = this;
    // 保留调用时传入的参数
    let args = arguments;
    // 记录本次触发回调的时间
    let now = +new Date();
    // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
    if (now - last < delay) {
      // 如果时间间隔小于我们设定的时间间隔阈值,则为本次触发操作设立一个新的定时器
      clearTimeout(timer);
      timer = setTimeout(function() {
        last = now;
        fn.apply(context, args);
      }, delay);
    } else {
      // 如果时间间隔超出了我们设定的时间间隔阈值,那就不等了,无论如何要反馈给用户一次响应
      last = now;
      fn.apply(context, args);
    }
  };
}
// 获取可视区域的高度
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
// 用新的 throttle 包装 scroll 的回调
const lazyload = throttle(() => {
  // 获取所有的图片标签
  const imgs = document.querySelectorAll('#list .wrap-img img');
  // num 用于统计当前显示到了哪一张图片,避免每次都从第一张图片开始检查是否露出
  let num = 0;
  for (let i = num; i < imgs.length; i++) {
    // 用可视区域高度减去元素顶部距离可视区域顶部的高度
    let distance = viewHeight - imgs[i].getBoundingClientRect().top;
    // 如果可视区域高度大于等于元素顶部距离可视区域顶部的高度,说明元素露出
    if (distance >= 100) {
      // 给元素写入真实的 src,展示图片
      let hasLaySrc = imgs[i].getAttribute('data-has-lazy-src');
      if (hasLaySrc === 'false') {
        imgs[i].src = imgs[i].getAttribute('data-src');
        imgs[i].setAttribute('data-has-lazy-src', true); // 
      }
      // 前 i 张图片已经加载完毕,下次从第 i+1 张开始检查是否露出
      num = i + 1;
    }
  }
}, 1000);


注意:给元素写入真实的 src 了之后,把 data-has-lazy-src 设置为 true ,是为了避免回滚的时候再设置真实的 src 时,浏览器会再请求这个图片一次,白白浪费服务器带宽。

微信图片_20220513153935.png


具体细节请看文件 文章列表


2.2 后端优化


后端用到的技术是 node、express 和 mongodb。


后端主要问题是接口速度很慢,特别是文章列表的接口,已经是分页请求数据了,为什么还那么慢呢 ?


所以查看了接口返回内容之后,发现返回了很多列表不展示的字段内容,特别是文章内容都返回了,而文章内容是很大的,占用了很多资源与带宽,从而使接口消耗的时间加长


微信图片_20220513154004.png


从上图可以看出文章列表接口只要返回文章的 标题、描述、封面、查看数,评论数、点赞数和时间即可。


所以把不需要给前端展示的字段注释掉或者删除。


// 待返回的字段
      let fields = {
        title: 1,
        // author: 1,
        // keyword: 1,
        // content: 1,
        desc: 1,
        img_url: 1,
        tags: 1,
        category: 1,
        // state: 1,
        // type: 1,
        // origin: 1,
        // comments: 1,
        // like_User_id: 1,
        meta: 1,
        create_time: 1,
        // update_time: 1,
      };


同样对其他的接口都做了这个处理。


后端做了处理之后,所有的接口速度都加快了,特别是文章列表接口,只用了 0.04 - 0.05 秒左右,相比之前的 4.3 秒,速度提高了 100 倍,简直不要太爽, 效果如下:


微信图片_20220513154044.png


此刻心情如下:


微信图片_20220513154056.gif


2.3 服务器优化


你以为前后端都优化一下,本文就完了 ?小兄弟,你太天真了,重头戏在后头 !


微信图片_20220513154109.jpg


笔者服务器用了 nginx 代理。


做的优化如下:


  • 隐藏 nginx 版本号

一般来说,软件的漏洞都和版本相关,所以我们要隐藏或消除 web 服务对访问用户显示的各种敏感信息。


如何查看 nginx 版本号? 直接看 network 的接口或者静态文件请求的 Response Headers 即可。


没有设置之前,可以看到版本号,比如我网站的版本号如下:


Server: nginx/1.6.2


设置之后,直接显示 nginx 了,没有了版本号,如下:


Server: nginx
  • 开启 gzip 压缩

nginx 对于处理静态文件的效率要远高于 Web 框架,因为可以使用 gzip 压缩协议,减小静态文件的体积加快静态文件的加载速度、开启缓存和超时时间减少请求静态文件次数。


笔者开启 gzip 压缩之后,请求的静态文件大小大约减少了 2 / 3 呢。


gzip on;
#该指令用于开启或关闭gzip模块(on/off)
gzip_buffers 16 8k;
#设置系统获取几个单位的缓存用于存储gzip的压缩结果数据流。16 8k代表以8k为单位,安装原始数据大小以8k为单位的16倍申请内存
gzip_comp_level 6;
#gzip压缩比,数值范围是1-9,1压缩比最小但处理速度最快,9压缩比最大但处理速度最慢
gzip_http_version 1.1;
#识别http的协议版本
gzip_min_length 256;
#设置允许压缩的页面最小字节数,页面字节数从header头得content-length中进行获取。默认值是0,不管页面多大都压缩。这里我设置了为256
gzip_proxied any;
#这里设置无论header头是怎么样,都是无条件启用压缩
gzip_vary on;
#在http header中添加Vary: Accept-Encoding ,给代理服务器用的
gzip_types
    text/xml application/xml application/atom+xml application/rss+xml application/xhtml+xml image/svg+xml
    text/javascript application/javascript application/x-javascript
    text/x-json application/json application/x-web-app-manifest+json
    text/css text/plain text/x-component
    font/opentype font/ttf application/x-font-ttf application/vnd.ms-fontobject
    image/x-icon;
#进行压缩的文件类型,这里特别添加了对字体的文件类型
gzip_disable "MSIE [1-6]\.(?!.*SV1)";
#禁用IE 6 gzip


把上面的内容加在 nginx 的配置文件 ngixn.conf 里面的 http 模块里面即可。


是否设置成功,看文件请求的 Content-Encoding 是不是 gzip 即可。


微信图片_20220513154239.png


  • 设置 expires,设置缓存


server {
        listen       80;
        server_name  localhost;
        location  / {
            root   /home/blog/blog-react/build/;
            index  index.html;
            try_files $uri $uri/ @router;
            autoindex on;
            expires 7d; # 缓存 7 天
        }
    }


我重新刷新请求的时候是 2019 年 3 月 16 号,是否设置成功看如下几个字段就知道了:


微信图片_20220513154307.png


  1. Staus Code 里面的 form memory cache 看出,文件是直接从本地浏览器本地请求到的,没有请求服务器。
  2. Cache-Control 的 max-age= 604800 看出,过期时间为 7 天。
  3. Express 是 2019 年 3 月 23 号过期,也是 7 天过期。


注意:上面最上面的用红色圈中的 Disable cache 是否是打上了勾,打了勾表示:浏览器每次的请求都是请求服务器,无论本地的文件是否过期。所以要把这个勾去掉才能看到缓存的效果。

终极大招:服务端渲染 SSR,也是笔者接下来的方向。


3.1 测试场景


一切优化测试的结果脱离了实际的场景都是在耍流氓,而且不同时间的网速对测试结果的影响也是很大的。


所以笔者的测试场景如下:


  • a. 笔者的服务器是阿里的,配置是入门级的学生套餐配置,如下:


微信图片_20220513154333.png


  • b. 测试网络为 10 M 光纤宽带。

3.2 优化结果


优化之后的首屏速度是 2.07 秒。


微信图片_20220513154346.png


最后加了缓存的结果为 0.388 秒


微信图片_20220513154359.png


再来看下 Lighthouse 的测试结果:


微信图片_20220513154416.png


比起优化之前,各项指标都提升了很大的空间。

相关文章
|
设计模式 SpringCloudAlibaba 负载均衡
每天打卡,跟冰河肝这些项目,技术能力嗖嗖往上提升
前几天,就有不少小伙伴问我,冰河,你星球有哪些项目呢?我想肝你星球的项目,可以吗?今天,我就给大家简单聊聊我星球里有哪些系统性的项目吧。其实,每一个项目的价值都会远超门票。
153 0
每天打卡,跟冰河肝这些项目,技术能力嗖嗖往上提升
技术人修炼之道阅读笔记(一)让自己更值钱的5个能力
技术人修炼之道阅读笔记(一)让自己更值钱的5个能力
|
3月前
|
开发者 CDN 监控
【破局·提速】当Vaadin遇上性能怪圈:开发者的智慧较量与极速加载的实战秘籍!
【8月更文挑战第31天】本文详细介绍了优化Vaadin应用性能的方法,特别是提高加载速度的实战技巧。首先分析性能瓶颈,如服务器响应时间和数据库查询效率等;然后通过代码优化、数据分页与急切加载技术减少资源消耗;接着利用资源压缩合并及CDN加速,进一步提升加载速度;最后通过持续性能监控和测试确保优化效果。通过综合应用这些策略,可显著改善用户体验。
76 0
|
4月前
|
运维 C# 开发工具
C#实战 | 天行健、上下而求索
【7月更文挑战第7天】使用C语言实现了一个小球(小方块)在屏幕上斜向移动并反弹的程序。当C#入门案例包括创建控制台应用和Windows窗体应用。 1. **控制台应用“天行健,君子以自强不息”** - 使用Visual Studio创建新C#控制台项目,命名为ConsoleAppStrengthenSelf。 - 在Main()方法中使用`Console.WriteLine()`输出励志语句。 - 运行程序,控制台显示结果。 每个项目都涉及Visual Studio的使用,Main()作为程序入口,以及不同类型的用户交互:控制台的文本输出和Windows窗体的图形界面。
47 0
C#实战 | 天行健、上下而求索
|
6月前
|
算法 开发者
代码与禅意:技术实践中的悟道之旅
【2月更文挑战第23天】在技术的海洋中,我们如同行者探索未知的领域。本文透过个人的技术实践经历,探讨了编程不仅仅是一门科学或艺术,更是一种哲学和内省的过程。我们将深入分析如何通过代码实现自我超越,以及在这个过程中对技术、生活和存在的深刻理解。
|
运维 算法 架构师
又爆新作!阿里甩出架构师进阶必备神仙笔记,底层知识全梳理
据有关数据表明,目前Java程序员这个群体的数量不减反增,行业内的竞争也是越来越严重。在同一时间入行的人,经过一段时间的学习后,差距就会显示出来。其实出现这样的原因大多数都是因为学习的方向出了问题。大多数人学Java刚开始只是为了快速就业,但是在工作了之后却没有一个好的学习路线,那些其实很重要的东西只是因为工作上用不到从而忽略掉了,慢慢的才发现自己与别人之间已经存在很大差距了!
|
安全 编译器 程序员
【C++修炼之路】1. 初窥门径(二)
【C++修炼之路】1. 初窥门径(二)
【C++修炼之路】1. 初窥门径(二)
|
存储 分布式计算 自然语言处理
【C++修炼之路】1. 初窥门径(一)
【C++修炼之路】1. 初窥门径(一)
【C++修炼之路】1. 初窥门径(一)
冰河与你聊聊大厂更加看重哪些能力?(建议收藏)
很多小伙伴问我是如何同时拿到 阿里、字节跳动、腾讯、京东、和美团百万年薪Offer的。今天我们就来简单的聊聊除了技术外,大厂还会看重哪些技能,从本质上说,除了技术,互联网大厂更看重这些基础能力!
173 0
冰河与你聊聊大厂更加看重哪些能力?(建议收藏)
|
Cloud Native 搜索推荐 中间件
在线教育行业,不可不知的阿里云中间件实战秘笈
基于开放的技术标准、理念和实践,云原生已经成为企业数字化转型的最短路径。中间件作为云原生的核心支点,凭借种类丰富、接入简单、稳定高效的核心优势,已经成为在线教育企业业务稳定、效率提升的“法宝”。 阿里云中间件正在以全面领先的技术、产品和最佳实践,引领在线教育进入新的阶段。
2384 3
在线教育行业,不可不知的阿里云中间件实战秘笈