likes
comments
collection
share

前端面试系列之性能优化和服务端篇

作者站长头像
站长
· 阅读数 11

之前整理了前端面试相关的知识,分享给大家!

注:很多内容来源于网络,侵权联系删除哈!

前端性能优化

性能指标相关有哪些?

  • fp、fcp、fmp(lcp)、tti、fci 等指标的意义与采集方法

CDN

  • CDN(Content Delivery Network,内容分发网络)是指一种通过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。

  • CDN的作用

    • CDN一般会用来托管Web资源(包括文本、图片和脚本等),可供下载的资源(媒体文件、软件、文档等),应用程序(门户网站等)。使用CDN来加速这些资源的访问。

    • (1)在性能方面,引入CDN的作用在于:

      • 用户收到的内容来自最近的数据中心,延迟更低,内容加载更快
      • 部分资源请求分配给了CDN,减少了服务器的负载
    • (2)在安全方面,CDN有助于防御DDoS、MITM等网络攻击:

      • 针对DDoS:通过监控分析异常流量,限制其请求频率
      • 针对MITM:从源服务器到 CDN 节点到 ISP(Internet Service Provider),全链路 HTTPS 通信
  • CDN的使用场景

    • 使用第三方的CDN服务: 如果想要开源一些项目,可以使用第三方的CDN服务
    • 使用CDN进行静态资源的缓存: 将自己网站的静态资源放在CDN上,比如js、css、图片等。可以将整个项目放在CDN上,完成一键部署。
    • 直播传送: 直播本质上是使用流媒体进行传送,CDN也是支持流媒体传送的,所以直播完全可以使用CDN来提高访问速度。CDN在处理流媒体的时候与处理普通静态文件有所不同,普通文件如果在边缘节点没有找到的话,就会去上一层接着寻找,但是流媒体本身数据量就非常大,如果使用回源的方式,必然会带来性能问题,所以流媒体一般采用的都是主动推送的方式来进行。

懒加载

如何避免回流与重绘?

  • 浏览器针对页面的回流与重绘,进行了自身的优化——渲染队列

    • 浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。
    • 上面,将多个读操作(或者写操作)放在一起,就会等所有的读操作进入队列之后执行,这样,原本应该是触发多次回流,变成了只触发一次回流。
  • CSS

    • 操作DOM时,尽量在低层级的DOM节点进行操作

      • 尽可能在DOM树的最末端改变class
    • 不要频繁操作元素的样式,对于静态页面,可以修改类名,而不是样式。

    • 不要使用table布局, 一个小的改动可能会使整个table进行重新布局

    • 使用absolute或者fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素

    • 多用visibility:hidden少用display:none

    • 使用css3硬件加速,可以让transform、opacity、filters等动画效果不会引起回流重绘

  • JS

    • 将元素先设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
    • 避免频繁操作DOM,可以创建一个文档片段documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中

图片优化

  • 不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用 CSS 去代替。

  • 对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。

  • 小图使用 base64 格式

  • 将多个图标文件整合到一张图片中(雪碧图)

  • 选择正确的图片格式:

    • 对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好
    • 小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替
    • 照片使用 JPEG

Webpack优化

  • 如何提⾼webpack的打包速度?

    • 优化 Loader

      • 对于 Loader 来说,影响打包效率首当其冲必属 Babel 了。因为 Babel 会将代码转为字符串生成 AST,然后对 AST 继续进行转变最后再生成新的代码,项目越大,转换代码越多,效率就越低。当然了,这是可以优化的。
      • 首先我们优化 Loader 的文件搜索范围
    • HappyPack

      • 受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。
      • HappyPack 可以将 Loader 的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了
    • DllPlugin

      • DllPlugin 可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。
      • 需要使用 DllReferencePlugin 将依赖文件引入项目中
      • autodll-webpack-plugin
    • 代码压缩

      • 在 Webpack3 中,一般使用 UglifyJS 来压缩代码,但是这个是单线程运行的,为了加快效率,可以使用 webpack-parallel-uglify-plugin 来并行运行 UglifyJS,从而提高效率。
      • 在 Webpack4 中,不需要以上这些操作了,只需要将 mode 设置为 production 就可以默认开启以上功能。
      • 代码压缩也是我们必做的性能优化方案,当然我们不止可以压缩 JS 代码,还可以压缩 HTML、CSS 代码,并且在压缩 JS 代码的过程中,我们还可以通过配置实现比如删除 console.log 这类代码的功能。
    • 通过 externals 配置来提取常⽤库

    • 其他

      • resolve.extensions:用来表明文件后缀列表,默认查找顺序是 ['.js', '.json'],如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在前面
      • resolve.alias:可以通过别名的方式来映射一个路径,能让 Webpack 更快找到路径
      • module.noParse:如果你确定一个文件下没有其他依赖,就可以使用该属性让 Webpack 不扫描该文件,这种方式对于大型的类库很有帮助
  • 如何减少 Webpack 打包体积

    • 按需加载

    • Scope Hoisting(作用域提升)

      • Scope Hoisting 会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去。

        • 如果在 Webpack4 中你希望开启这个功能,只需要启用 optimization.concatenateModules 就可以了
      • 是把多个模块的代码合并到一个函数作用域中执行。在这一过程中,webpack会按照顺序正确的合并模块代码,同时对设计的标识符做适当处理以避免重名。

      • 减少了函数调用,对运行效率有一定提升,同时也降低了打包体积。

    • Tree Shaking

      • Tree Shaking 可以实现删除项目中未被引用的代码
      • 依赖于ES6 moudel特性
  • 如何⽤webpack来优化前端性能?

    • 压缩代码:删除多余的代码、注释、简化代码的写法等等⽅式。可以利⽤webpack的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩JS⽂件, 利⽤ cssnano (css-loader?minimize)来压缩css
    • 利⽤CDN加速: 在构建过程中,将引⽤的静态资源路径修改为CDN上对应的路径。可以利⽤webpack对于 output 参数和各loader的 publicPath 参数来修改资源路径
    • Tree Shaking: 将代码中永远不会⾛到的⽚段删除掉。可以通过在启动webpack时追加参数 --optimize-minimize 来实现
    • Code Splitting: 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览器缓存
    • 提取公共第三⽅库: SplitChunksPlugin插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的公共代码

服务端

Node.js

  • 怎么看 nodejs 可支持高并发

    • nodejs 的单线程架构模型

      • nodejs 其实并不是真正的单线程架构,因为 nodejs 还有I/O线程存在(网络I/O、磁盘I/O),这些I/O线程是由更底层的 libuv 处理,这部分线程对于开发者来说是透明的。 JavaScript 代码永远运行在V8上,是单线程的。

      • 单线程架构的优势和劣势:

        • 优势:

          • 单线程就一个线程在玩,省去了线程间切换的开销
          • 还有线程同步的问题,线程冲突的问题的也不需要担心
        • 劣势:

          • 劣势也很明显,现在起步都是 4 核,单线程没法充分利用 cpu 的资源
          • 单线程,一旦崩溃,应用就挂掉了,大家调试脚本也知道一旦执行过程报错了,本次调试就直接结束了
          • 因为只能利用一个 cpu ,一旦 cpu 被某个计算一直占用, cpu 得不到释放,后续的请求就会一直被挂起,直接无响应了
          • 当然这些劣势都已经有成熟的解决方案了,使用 PM2 管理进程,或者上 K8S 也可以
    • 核心:事件循环机制

      • 那你个单线程怎么支持高并发呢?
      • 核心就要在于 js 引擎的事件循环机制
      • 浏览器和 nodejs 的事件循环是稍有区别的,先给面试官简单说下事件循环的核心,执行栈、宏队列和微队列
      • 然后重点说 nodejs 事件循环的差异点
    • 给出个结论 nodejs 是异步非阻塞的,所以能扛住高并发

      • 举例

        • 比如有个客户端请求A进来,需要读取文件,读取文件后将内容整合,最后数据返回给客户端。但在读取文件的时候另一个请求进来了,那处理的流程是怎么样的?

          • 请求A进入服务器,线程开始处理该请求
          • A 请求需要读取文件,ok,交给文件 IO 处理,但是处理得比较慢,需要花 3 秒,这时候 A 请求就挂起(这个词可能不太恰当),等待通知,而等待的实现就是由事件循环机制实现的,
          • 在A请求等待的时候,cpu 是已经被释放的,这时候B请求进来了, cpu 就去处理B请求
          • 两个请求间,并不存在互相竞争的状态。那什么时候会出现请求阻塞呢?涉及到大量计算的时候,因为计算是在 js 引擎上执行的,执行栈一直卡着,别的函数就没法执行,举个栗子,构建一个层级非常深的大对象,反复对这个这个对象 JSON.parse(JSON.stringify(bigObj))
    • 另外可以扩展 同步、异步、阻塞、非阻塞 这个几个概念

      • 同步和异步关注的是消息通信机制。

        • 同步:在发起一个调用后,在没有得到结果前,该调用不返回,知道调用返回,才往下执行,也就是说调用者等待被调用方返回结果。
        • 异步:在发起一个调用后,调用就直接返回,不等待结果,继续往下执行,而执行的结果是由被调用方通过状态、通知等方式告知调用方,典型的异步编程模型比如 Node.js
      • 阻塞和非阻塞,关注的是在等待结果时,线程的状态。

        • 阻塞:在等待调用结果时,线程挂起了,不往下执行
        • 非阻塞:与上面相反,当前线程继续往下执行
  • 事件循环

    • Timers: 定时器 Interval Timoout 回调事件,将依次执行定时器回调函数

      • 执行定时器的回调,但注意,在 node 11 前,连续的几个定时器回调会连续的执行,而不是像浏览器那样,执行完一个宏任务立即执行微任务。
    • Pending: 一些系统级回调将会在此阶段执行

    • Idle,prepare: 此阶段"仅供内部使用"

    • Poll: IO回调函数,这个阶段较为重要也复杂些:

      • 先查看 check 阶段是否有事件,有的话执行
      • 执行完 check 阶段后,检查 poll 阶段的队列是否有事件,若有则执行
      • poll 的队列执行完成后,执行 check 阶段的事件
    • Check: 执行 setImmediate() 的回调

      • 这个阶段执行 setImmediate() 的回调,这个事件只在 nodejs 中存在。
    • Close: 执行 socket 的 close 事件回调

    • process.nextTick

      • 关于 process.nextTick ,这个事件的优先级要高于其他微队列的事件,所以对于需要立即执行的回调事件可以通过该方法将事件放置到微队列的起始位置。
  • nodejs 怎么创建进程线程,可以用在哪些场景

    • 如何开启多个子进程

      • 本质还是通过 child_process.fork() 专门用于衍生新的 Node.js 进程,衍生的 Node.js 子进程独立于父进程,但两者之间建立的 IPC 通信通道除外, 每个进程都有自己的内存,带有自己的 V8 实例

      • 单线程的一个缺点是不能充分利用多核,所以官方推出了 cluster 模块, cluster 模块可以创建共享服务器端口的子进程

const cluster = require('cluster');
for (let i = 0; i < numCPUs; i++) {
    cluster.fork(); // 生成新的工作进程,可以使用 IPC 和父进程通信
}
  • 如何在一个进程的前提下开启多个线程
    • 在 nodejs 10.0 及以上的版本,新增了 worker_threads 模块,可开启多个线程
const {
    Worker, isMainThread, parentPort, workerData
} = require('worker_threads');
const worker = new Worker(__filename, {
    workerData: script
});
  • 线程间如何传输数据: parentPort postMessage on 发送监听消息
  • 共享内存: SharedArrayBuffer 通过这个共享内存
    • 使用场景
      • 对于服务中涉及大量计算的,可以开启一个工作线程,由这个线程去执行,执行完毕再把结果通知给服务线程。
      • 常见的一个场景,在服务中若需要执行 shell 命令,那么就需要开启一个进程
var exec = require('child_process').exec;
exec('ls', function(error, stdout, stderr){
    if(error) {
        console.error('error: ' + error);
        return;
    }
    console.log('stdout: ' + stdout);
});
  • 介绍下 stream

    • 流在 nodejs 用的很广泛,但对于大部分开发者来说,更多的是使用流,比如说 HTTP 中的 request respond ,标准输入输出,文件读取(createReadStream), gulp 构建工具等等。

    • 流,可以理解成是一个管道,比如读取一个文件,常用的方法是从硬盘读取到内存中,在从内存中读取,这种方式对于小文件没问题,但若是大文件,效率就非常低,还有可能内存不足,采用流的方式,就好像给大文件插上一根吸管,持续的一点点读取文件的内容,管道的另一端收到数据,就可以进行处理,了解 Linux 的朋友应该非常熟悉这个概念。

    • Node.js 中有四种基本的流类型:

      • Writable - 可写入数据的流(例如 fs.createWriteStream())。
      • Readable - 可读取数据的流(例如 fs.createReadStream())。
      • Duplex - 可读又可写的流(例如 net.Socket)。
      • Transform - 在读写过程中可以修改或转换数据的 Duplex 流(例如zlib.createDeflate())。接触比较多的还是第一二种 pipe 来消费可读流
  • 位 字节的关系

    • 位:bit 代表二进制 字节:1字节 = 8位

Koa

  • 洋葱模型

    • 我们通过 use 注册中间件,中间件函数有两个参数第一个是上下文,第二个是 next,在中间件函数执行过程中,若遇到 next() ,那么就会进入到下一个中间件中执行,下一个中间执行完成后,在返回上一个中间件执行 next() 后面的方法,这便是中间件的执行逻辑。
  • 手写 Compose