面试官:聊一聊前端性能优化

发布于:2024-04-26 ⋅ 阅读:(14) ⋅ 点赞:(0)

前言

性能优化总是被面试官问,每次都只能答出那么几个,其实大家都清楚性能优化手段非常多,只要是能让用户体验更好的手段都可以称之为性能优化,本期我就总结下我所知道的性能优化手段,欢迎各位补充~

一、减少 http 请求

比如一个项目中,登录之后需要展示登录时的昵称,这个昵称就不需要重新发一次接口请求去拿到了,可以通过路由传参拿到

为何说减少 http 请求是一种性能优化?因为一个 http 的请求中间的过程非常多,常见的 输入url后到页面展示的过程 大家都清楚,步骤很多

这个过程还不清楚的,可以跳转这两篇文章,都写得很详细

二、使用 http2.0

当然,现在基本上都是用 http2.0 这个版本的 http 了,那为什么使用 http2.0 会性能更优?

http2.0 更优是相对于此前的版本,此前 1.1 版本因为有多个 keep-alive 长连接导致了 http 的队头阻塞问题,同时多个长连接也带来了 带宽 用不满的问题

2.0 针对 1.1 的这些问题,在一个域名下,多个 tcp 长连接合并成了一个,这就是多路复用,并且 2.0 采用了二进制分帧层,将每个请求分成了一帧一帧的数据进行传输并打上标记,可以给特定的数据帧加急处理

同样,这个问题我在文章 中讲得非常详细了

三、使用 SSR 服务端渲染

SSR 可以让首屏加载更快,带来更好的 SEO

前端基本上现在都是 SPA 单页应用,单页应用的缺陷就是首屏加载很慢。使用 SSR 服务端渲染可以带来更好的 SEOSEO 就是搜索引擎优化,搜索引擎就是爬虫,可以更好的爬数据

感觉掘金在 seo 上比不过 csdn ,每次搜文章, csdn 永远在前面😭

其实早期的前后端不分离开发方式就是服务端渲染,就是 jsp ,后端直接向前端返回一个 html 文件,既然如此为何如今又要搞一个分离式开发方式,这是为了开发效率,开发效率的优点受益比服务端渲染高,不分离开发方式前端工作量太少了,效率很低

下面可以看下 vue 是如何做 ssr

vue - ssr

其实 vuessr 在官网上就有

1.png

我们可以在后端新建一个文件运行这段代码试试

// 此文件运行在 Node.js 服务器上
import { createSSRApp } from 'vue'
// Vue 的服务端渲染 API 位于 `vue/server-renderer` 路径下
import { renderToString } from 'vue/server-renderer'

const app = createSSRApp({
  data: () => ({ count: 1 }),
  template: `<button @click="count++">{{ count }}</button>`
})

renderToString(app).then((html) => {
  console.log(html)
})

打印输出:<button>1</button>

createSSRApp 可以帮我们创建一个组件,这样我们就可以不需要 vue 文件创建 vue 组件了,这个组件里面可以创建数据源,模板等,然后借助 renderToString 帮我们把模板当成字符串渲染成 html

接下来借助 express 来实现一个后端 demo

express 相比 koaexpress 的路由无需另外安装, koa 其实是基于 express 打造的

import express from 'express'
import { createSSRApp } from 'vue'
import { renderToString } from 'vue/server-renderer'

const server = express() // 不需要new

server.get('/', (req, res) => {
  const app = createSSRApp({
    data: () => ({ count: 1 }),
    template: `<button @click="count++">{{ count }}</button>`
  })

  renderToString(app).then((html) => {
    res.send(`
    <!DOCTYPE html>
    <html>
      <head>
        <title>Vue SSR Example</title>
      </head>
      <body>
        <div id="app">${html}</div>
      </body>
    </html>
    `)
  })
})

server.listen(3000, () => {
  console.log('ready')
})

server.get 就是路由,里面的逻辑是创建一个 vue 组件,然后渲染成字符串,返回给浏览器,这就是 jspjsp 就是 ssr ,现在就可以访问 localhost:3000 看到一个 html 页面了,里面是一个按钮

这个按钮目前点击是不会生效的,因为浏览器端没有 vue 的请求,还需要将 app 挂载 amount#app 上,我们还需要在项目根目录下新建一个 app.js ,如下,目的是在服务端和客户端之间共享

// app.js (在服务器和客户端之间共享)
import { createSSRApp } from 'vue'

export function createApp() {
  return createSSRApp({
    data: () => ({ count: 1 }),
    template: `<button @click="count++">{{ count }}</button>`
  })
}

根目录下新建一个 client.js

// client.js
import { createApp } from './app.js'

createApp().mount('#app')

这么做就是把 server.js 中定义组件的代码搬出去写了

server.jshtml 中引入 vue 源码,再引入 client.js ,再引入路径

import express from 'express'
import { renderToString } from 'vue/server-renderer'
import { createApp } from './app.js'

const server = express()

server.get('/', (req, res) => {
  const app = createApp()

  renderToString(app).then((html) => {
    res.send(`
    <!DOCTYPE html>
    <html>
      <head>
        <title>Vue SSR Example</title>
        <script type="importmap">
          {
            "imports": {
              "vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
            }
          }
        </script>
        <script type="module" src="/client.js"></script>
      </head>
      <body>
        <div id="app">${html}</div>
      </body>
    </html>
    `);
  });

  app.mount('#app')
})

server.use(express.static('.'))

server.listen(3000, () => {
  console.log('ready')
})

就跟着 vue 官方文档一步一步就实现了一个 ssr 的 demo ,现在访问 localhost:3000 点击按钮可以实现累加的效果,也就是是实现了响应式

1.gif

然后你可以去查看页面源代码, button 可以看到,若是 vue 的项目,你是看不到任何标签的,这样搜索引擎这样的爬虫就可以爬到你的数据,假设这是个买衣服的网站,你就可以尽可能地把信息展现到 html 中来

2.png

像是这样的实现,其实我们就可以把这个按钮组件写成一个登录组件,然后点击后跳转到另一个页面,那个页面又可以写其他端口,这样就是解决了首屏加载过慢的问题

公司做的产品只要是给用户用得,基本上都会去做 ssr

四、合理使用 cdn

CDN(Content Delivery Network)全称为内容分发网络

像是 cdn 就涉及到服务器分布的问题了,谷歌的服务器放在深海中的,为了方便散热🤣。像是我们访问国外的网站,就算有魔法加持也会比较慢,就是因为一个网络请求跨越的距离太远了。像是国内,假设阿里的服务器在杭州,西藏的朋友访问淘宝就会有点慢,因此,这些大厂基本上都会再设置云服务器方便偏远地区访问。

合理使用 cdn ,为何要说合理使用,就是因为服务器资源太贵了,不可能每个城市都给你搞一个云服务器

合理使用 cdn 是运维部门该干的事情,和前后端无关

当然不排除某些公司或者小厂,为了降本增效,运维工作同时交给了后端来干。另外前后端会有一个测试服务器,可以随便测自己的项目。

五、将 css 放在文件头部,将 js 放在文件底部

css 放在 body 当中其实也可以,这么带来的效果确实 html 优先出现在用户面前,但是没有样式,降低用户体验!因此 css 放到文件头部非常重要

有时候页面炸了的时候,没有样式,全是文字和🔗,非常影响用户体验,这种效果还不如给用户看白屏

当然 js 也可以写在上面,得看情况,若 js 在页面加载中需要用到,就需要放到前面,这种情况很少。 js 引擎线程和浏览器渲染线程是互斥的,因为 js 也可以操作 dom 结构,可能会冲突

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>

    </style>
    <script>
        // 放在这里意味着页面加载过程中需要用到这个js,情况会比较少
    </script>
</head>
<body>
    <div id="app">hello</div>

    <script src="***">
        // 这个适合cdn引入,比如引入vue
    </script>
    <script>
        
    </script>
</body>
</html>

当然,你也可以用 asyncdefer 解决 js 阻塞问题

async & defer

二者的共同点均是异步加载 js

js 是有两个步骤的,先是加载,后是执行

defer 是延迟的意思,就是让 js 延迟执行,等 html 加载完毕再去执行,因此 defer 的效果就是把 js 放到文件底部

async 不会延迟执行 js ,它的效果是 js 照常加载,加载完毕就需要立即执行 js ,期间 html 就不能执行了

因此 defer 看样子更好说话, js 加载完毕等 html 执行完毕再去执行 js ,而 async 就难说话一点,加载完 js 后立即执行 jsdefer 带来的效果其实就是将 js 放到文件底部, async 的效果就是放在文件头部

六、使用精灵图(雪碧图)

精灵图(雪碧图)就是一种将多个小图标或图像组合到单个图像文件中的技术

有个很经典的🌰就是豆瓣官网右上角一排的文字图片

3.png

既然是一张图那如何实现可以点到每个 li 的呢

4.png

这张图并不是直接完整的放到页面中去,否则无法实现各点各的,这些个 li 其实都用到了这张图,只是每个 li 都会单独去调整这张图的位置,比如读书用到了这张图,只把读书二字展现了出来

这么做的意义就是减少 http 的请求次数,若这 8 个栏目都用自己单独的图片,就意味着要发 8 个 http 请求,用雪碧图就一次请求多处使用

ui 设计师若是不懂这个优化,我们前端可以跟她提这个需求,做成一张图

七、善用 http 缓存:强缓存 & 协商缓存

关于这两个缓存,此前专门出过一期文章详细讲解过:,下面这段话不太理解可以详细看下这篇

二者都是后端控制的东西,强缓存是响应头添加 'Cache-Control': 'max-age=xxx' 字段, max-age 是过期时间,强缓存后无法缓存输入 url 后的 get 请求,想要缓存这个请求需要靠协商缓存来实现,协商缓存的实现是在强缓存的基础上添加一个 'Last-Modified': stats.mtimeMs 或者 etag 字段,若检查到前端返回的 If-Modified-Since 时间一致,后端就返回 304 状态码给前端,浏览器就从缓存中读取静态资源

八、压缩文件

压缩文件前后端都可以做,前端压缩就是打包,将代码打包成密密麻麻的样子,剔除掉无用的换行,空格等,这样文件大小就可以变小,文件变小下载速度就会快

后端的压缩就是向前端返回静态资源进行压缩,比如图片转格式等等

九、懒加载

懒加载的核心思想就是图片不在可视区范围中不加载,滚到可视区范围内才给予加载。这么做就是没必要页面初次加载所有图片

懒加载的实现早期就出过一篇文章:

懒加载的实现需要获取到可视区范围的高度,以及每张图片的高度,监听用户滚动的过程中图片是否进入范围内,进入时才赋值 srcsrc 只要有值就一定会发送 http 请求,此前存放 src 的属性可以任意取名,当然一般我们取名为 data- 前缀,比如下面这样

<img src="" data-src="****">

IntersectionObserver

IntersectionObserver 这个 api 也可以用于实现懒加载,这个方法可以用来监听目标元素和祖先元素是否相交,交叉多少都可以实现

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .img-item {
            display: block;
            height: 300px;
            margin-top: 50px;
        }
    </style>

</head>

<body>

    <body>
        <img class='img-item' src=""
            data-original="https://th.bing.com/th?id=ORMS.5053ecdbef05fa7726aa489d27b52e40&pid=Wdp&w=612&h=304&qlt=90&c=1&rs=1&dpr=1.25&p=0"
            alt="">
        <img class='img-item' src=""
            data-original="https://th.bing.com/th?id=ORMS.c5db2c88af1a76f18d0efe02fcded91d&pid=Wdp&w=612&h=304&qlt=90&c=1&rs=1&dpr=1.25&p=0"
            alt="">
        <img class='img-item' src=""
            data-original="https://th.bing.com/th?id=ORMS.c5129de8701c4a933d92cd6bf832b233&pid=Wdp&w=300&h=156&qlt=90&c=1&rs=1&dpr=1.25&p=0"
            alt="">
        <img class='img-item' src=""
            data-original="https://th.bing.com/th?id=ORMS.afe7f6448d6eba0055cd8ce9ac9fdf62&pid=Wdp&w=300&h=156&qlt=90&c=1&rs=1&dpr=1.25&p=0"
            alt="">
        <img class='img-item' src=""
            data-original="https://th.bing.com/th?id=ORMS.e168b9c5da30772083104ed0f4ef0ecf&pid=Wdp&w=612&h=304&qlt=90&c=1&rs=1&dpr=1.25&p=0"
            alt="">
        <img class='img-item' src=""
            data-original="https://th.bing.com/th?id=ORMS.8025ce5a977b3826589022cede69e110&pid=Wdp&w=300&h=156&qlt=90&c=1&rs=1&dpr=1.25&p=0"
            alt="">
        <img class='img-item' src=""
            data-original="https://th.bing.com/th?id=ORMS.a58ae29e32e20a27d498eed19528ee3c&pid=Wdp&w=300&h=156&qlt=90&c=1&rs=1&dpr=1.25&p=0"
            alt="">
        <img class='img-item' src=""
            data-original="https://th.bing.com/th?id=ORMS.2049b527600b31b2cd863a380be59848&pid=Wdp&w=300&h=156&qlt=90&c=1&rs=1&dpr=1.25&p=0"
            alt="">
        <img class='img-item' src=""
            data-original="https://th.bing.com/th?id=ORMS.9f51912b8b6c19a9891b380ad526db85&pid=Wdp&w=612&h=304&qlt=90&c=1&rs=1&dpr=1.25&p=0"
            alt="">
        <img class='img-item' src=""
            data-original="https://th.bing.com/th?id=ORMS.1b6375ea147b5704f9d073a326e1fc2a&pid=Wdp&w=300&h=156&qlt=90&c=1&rs=1&dpr=1.25&p=0"
            alt="">

        <script>
            const io = new IntersectionObserver((entries) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting) {
                        entry.target.src = entry.target.dataset.original
                        entry.target.removeAttribute('data-original')
                        io.unobserve(entry.target)
                    }
                })
            })
            const imgs = document.querySelectorAll('img[data-original]')
            imgs.forEach(item => {
                io.observe(item)
            })
        </script>
    </body>
</body>

</html>

当观察到一个图片元素进入视口时,触发 IntersectionObserver 的回调函数。在回调函数中,首先检查触发事件的元素是否进入了视口entry.isIntersecting,如果是,则将该元素的 src 属性设置为 data-original 属性的值,即加载图片。然后,移除 data-original 属性,以确保图片只加载一次。最后,调用 io.unobserve(entry.target) 停止对该元素的观察,避免不必要的性能消耗。

十、尽量用 css ,字体来代表图片

有些背景图就是一个渐变色,这个时候就不需要放图片了, css 能够实现就用 css

反正能用 css 就用 css ,能不用图片就不用图片。当然对于程序员来讲,谁不想早点下班呢,用 css 就意味着自己多点工作,一张图片多简单

十一、使用 webp 格式的图片

webp 格式的图片是谷歌推出的,这种格式的图像压缩算法能力要优于传统的 jpgpng 等格式,在相同图片质量的情况下,空间大小会优化 30% 左右的样子

关于图片的性能优化就是小图用雪碧图,大图用 webp 格式

十二、webpack:tree-shaking | 打包文件名 + hash

webpack 一般来说都是配置好的,默认配置的中规中矩,不算差也不算好,我们可以在此基础上增加一个 tree-shakingtree-shaking 的作用就是帮我们把项目中无用的代码给找出来,比如我们调试用的 console.log ,其实 console.log 对浏览器的开销还是蛮大的

以及项目中打包后的文件名被拼接了一个哈西值,这样就能剔除上一次打包的内容,或者做一个动态的组件引入

像是打包文件生成的 dist 目录,里面的文件默认就会有 hash 值, vite 默认会有这个操作,但是其他的打包工具就不一定会有

在实际开发中,打包工具不是由自己决定的,公司会统一好,不可能别人用 webpack ,你一个人用 vite

十三、尽量减少回流重绘

这个问题此前也详细讲过:

回流(重排)就是计算布局,重绘就是给页面上色

尽量不用 js 去直接修改 css

我们可以看下下面两种情况

// 案例一
box.style.width = '200px'
// 案例二
.more{
	width: '200px'
}
box.classList.add('more')

第一种方案就是直接修改 css ,第二种是添加类名。方案一会导致回流,方案二不会导致回流,因为添加类名并没有修改几何属性,它是间接交给了 css ,上面就说了, css 一般放在文件顶部,提前加载好了,因此浏览器已经准备好了,做好了回流这个计算,就是等你把类名加上去

display: none

当涉及需要对 dom 进行一系列的操作时,可以先利用 display: nonedom 脱离文档流,再修改 display: block 带回文档流

fragment

这是文档虚拟片段, js 中被当成 dom ,但是在 css 中又不会当成真实的 dom 加载出来,涉及多个操作 dom 时,可以对 fragment 操作,最后把 fragment 挂到真实 dom

clone

深克隆节点,对副本进行操作,最后将副本插入到文档中进行回流,跟前面的方法原理是一致的,具体语法上面晾出的文章都详细讲过

十四、事件委托

同样,事件委托之间也单独拿出来讲过 ,这里就不大费周章重复去讲

事件委托的机制是借助冒泡机制,把原本需要批量操作子组件的操作代理到一个父组件上

十五、if-else VS switch

当涉及到多个判断条件时,我们可以用 switch 去写判断语句

比如下面两个案例

// 检查成绩等级(if-else)
function checkGrade(score) {
    let grade;
    if (score >= 90) {
        grade = 'A';
    } else if (score >= 80) {
        grade = 'B';
    } else if (score >= 70) {
        grade = 'C';
    } else if (score >= 60) {
        grade = 'D';
    } else {
        grade = 'F';
    }
    return grade;
}

// 检查成绩等级(使用 switch)
function checkGradeSwitch(score) {
    let grade;
    switch (true) {
        case (score >= 90):
            grade = 'A';
            break;
        case (score >= 80):
            grade = 'B';
            break;
        case (score >= 70):
            grade = 'C';
            break;
        case (score >= 60):
            grade = 'D';
            break;
        default:
            grade = 'F';
    }
    return grade;
}

if-else 有个判断顺序的,一定是从上往下走逐个走到目标,每次都判断一下,浪费性能。而 switch 不然, switch 是直接命中目标,只有一次判断

if-else 会更加灵活,但是性能又没有 switch 来得好

十六、requestAnimationFrame避免页面卡顿

下面用 js 实现一个效果:小球滚动

2.gif

用一个定时器 setInterval ,让 left 值每间隔 16ms 就去自增

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .box {
            width: 200px;
            height: 200px;
            border-radius: 50%;
            background-color: red;
            position: relative;
        }
    </style>
</head>
<body>
    <div class="box"></div>

    <script>
        let box = document.querySelector('.box')

        let left = 0
        setInterval(() => {
            left++
            box.style.left = left + 'px'
        }, 16)  
    </script>
</body>
</html>

上面展示的动画其实是有卡顿的,当然,光看这个效果肯定大部分是 gif 的原因,为何 16ms 也会有卡顿?屏幕设计成 60Hz 不就是因为人眼最低的时间辨别时间就是 16.7ms 么,确实,但是 1616.7 就是不匹配,这个小球从第 A 位置移到 B 位置确实不会卡,但是下一次开始,移动的时候页面刷新一下,此时才会带来卡顿

这个时候我们就去采用 requestAnimationFrame ,这也是个定时器,这个定时器的执行时间不需要我们设置,它是根据屏幕刷新率来定的, 60Hz 就是 16.7ms 执行一次。

关于宏微任务,这个方法有歧义,暂且不讨论。总之这个方法的设计就是在每一帧渲染完准备去显示下一帧去执行的,它就是个动画帧

let box = document.querySelector('.box')

let left = 0

function fn () {
    left++
    box.style.left = left + 'px'
    window.requestAnimationFrame(fn) 
}
window.requestAnimationFrame(fn) 

采用这个方法实现的动画将会非常丝滑,这里用 gif 不便展示其效果

十七、Web Worker 开启多线程

js 开多线程是这个话题中非常加分的点

js 默认情况下是单线程,但是 v8 引擎执行 js 的时候是可以开辟多线程的

像是页面上的图片有水印一般都是页面加载的时候实现的,而非图片本身就有水印,像这种操作就是交给另一个线程来实现的

接下来实现一个效果带你理解多线程:点击上传图片,将图片变成黑白色

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <img src="" alt="" id="previewImage">
    <input type="file" id="imageInput">

    <script>
        const previewImage = document.getElementById('previewImage');
        const imageInput = document.getElementById('imageInput')

        imageInput.addEventListener('change', (e) => {
            const file = e.target.files[0]
            if (file) {
                const render = new FileReader() // js中专门用于读取上传图像文件的
                render.readAsArrayBuffer(file) // 将文件读成二进制流
                render.onload = (e) => {
                    const imageData = e.target.result // 二进制流
                    createImageBitmap(new Blob([imageData])).then((imageBitmap) => { // 将buffer流转成真实的数组,且是异步方法
                        console.log(imageBitmap);
                        // 其他逻辑:比如你想要实现水印
                        
                    }) 
                }
            }
        })
    </script>
</body>
</html>

代码先写到这里,这里代码还没有实现黑白效果。举个例子,以预览为例,预览对于用户而言,其实慢点是没有关系的,也就是说这个功能不应该阻塞线程,因此像是这样的逻辑,如果你正常往下实现,是会占用这个线程的。

或者说,假设你把像是预览图片的代码写在 js 主线程中,若预览需要 3s ,此时下面假设还有其他按钮,这 3s 内,你就无法进行其他操作了

接下来就把像是预览这样的操作交给第二个线程来实现,这里实现的是变黑白

render.onload = (e) => {
    const imageData = e.target.result // 二进制流

    // 开一个新的线程
    const worker = new Worker('worker.js')

    createImageBitmap(new Blob([imageData])).then((imageBitmap) => { // 将buffer流转成真实的数组,且是异步方法
        worker.postMessage({ imageBitmap }, [imageBitmap]) // 将目前的图片数据传给第二个线程

        worker.onmessage = () => { // 从第二个线程中获取数据

        }
    }) 
}

postMessgeonmessge 上一次见过,可以用管道通信的方式实现深拷贝

这两个方法是异步的,这个异步是针对主线程而言,但是里面的耗时是交给了另一个线程

接下来同级目录下新建一个文件 worker.js ,我们在这里面实现其他逻辑

我们可以试着在 worker.js 中打印 this 或者 self 看看指向, self 就是 this ,我们在这身上监听 message 事件

// console.log(self);
self.addEventListener('message', (e) => {
    console.log(e);
})

我们打印 e ,你会发现主线程中的 imageBitmap 在里面,这就是因为 self 监听到了主线程 postMessage 发布过来的 imageBitmap

5.png

// console.log(self);
self.addEventListener('message', (e) => {
    console.log(e.data.imageBitmap); // 获取到了主线程的图片资源
    const imageBitmap = e.data.imageBitmap;

    createImageBitmap(processImage(imageBitmap)) // processImage就是取到进程中的资源处理成canvas需要的格式
})

processImage 就是将参数转成 Uint8Array 二进制数组

es8 新推出了一个 就是专门用于存放数据流的格式

接下来实现 processImage 这个函数,这个函数需要做的就是将从主线程读到的文件流变成黑白,通过 canvas 实现,最终 worker.js 如下

// console.log(self);
self.addEventListener('message', (e) => {
    // console.log(e.data.imageBitmap); // 获取到了主线程的图片资源
    const imageBitmap = e.data.imageBitmap;

    createImageBitmap(processImage(imageBitmap)).then(processImageBitmap => {  // processImage就是取到进程中的资源处理成canvas需要的格式
        console.log(processImageBitmap);
        self.postMessage({processImageBitmap}, [processImageBitmap]) // 触发主线程的onmessage
    }) 
})

function processImage(inputImageBitmap) {
    const canvas = new OffscreenCanvas(inputImageBitmap.width, inputImageBitmap.height) // 实例化一个画布

    const ctx = canvas.getContext('2d') 
    ctx.drawImage(inputImageBitmap, 0, 0)

    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
    const inputData = new Uint8Array(imageData.data.buffer)
    // console.log(inputData); // 每四个一组,一组表示一个16进制像素点
    const outputData = new Uint8Array(inputData.length) // 创建一个大小相同的容器

    // 变黑白
    for (let i = 0; i < inputData.length; i+=4) {
        const avg = (inputData[i] + inputData[i + 1] + inputData[i + 2]) / 3
        outputData[i] = avg // 取平均值,颜色就不会很深或者很浅
        outputData[i + 1] = avg
        outputData[i + 2] = avg
        outputData[i + 3] = inputData[i + 3] // 保留颜色饱和度
    }

    return new ImageData(new Uint8ClampedArray(outputData.buffer), canvas.width, canvas.height)
}

这些过程中用上 canvas 纯粹是因为 canvas 身上有个方法 toDataURL 可以把 image 转成真实的 url 供你放到 img 上展示,这个 url 其实就是 base64 格式

createImageBitmap(new Blob([imageData])).then((imageBitmap) => { // 将buffer流转成真实的数组,且是异步方法
    worker.postMessage({ imageBitmap }, [imageBitmap]) // 将目前的图片数据传给第二个线程

    worker.onmessage = (e) => { // 从第二个线程中获取数据
        console.log(e.data.processImageBitmap);
        const processImageBitmap = e.data.processImageBitmap
        const previewCanvas = document.createElement('canvas')
        previewCanvas.width = processImageBitmap.width
        previewCanvas.height = processImageBitmap.height

        const previewCtx = previewCanvas.getContext('2d')
        previewCtx.drawImage(processImageBitmap, 0, 0) // 往previewCtx上绘制
        console.log(previewCanvas.toDataURL());
    }
})

我们可以打印看下这个 url,此时的图片已经 worker.js 处理回来了,黑白格式

6.png

最后把这个 base64url 放入到 src 中即可

previewImage.src = previewCanvas.toDataURL()

7.png

面试官:如何将一个图片转成 base64 格式

顺带提这个面试题,看到之前有人被问到过

canvas 就可以实现

let img = new Image(); // 创建一个新的 Image 对象

img.src = 'image.jpg'; // 设置图片的源

img.onload = function() { // 当图片加载完成后执行操作
  var canvas = document.createElement('canvas'); // 创建一个 Canvas 元素
  var ctx = canvas.getContext('2d');
  
  // 设置 Canvas 元素的宽高与图片一致
  canvas.width = img.width;
  canvas.height = img.height;
  
  // 在 Canvas 上绘制图片
  ctx.drawImage(img, 0, 0);
  
  // 将 Canvas 内容转换为 base64 格式
  var base64Data = canvas.toDataURL('image/jpeg');
  
  // base64Data 就是转换后的 base64 格式的图片数据
  console.log(base64Data);
};

除了给图片变换其他效果需要开启多线程外,其余还有很多情景,比如一个 for 循环需要循环一万次产生了一个阻塞,我们就可以把这个任务交给另一个线程来实现

十八、 css 选择器复杂性要低

我们看下面这案例

#app .text p{

}

浏览器读取 css 是从右往左读,因此上面的代码,就是先读到 p 标签,再是 text 类名,最后是 id

这样就是先去找到所有的 p 标签,然后再去找带有 text 类名的部分,最后再去找还得带有 app 这个 id 的部分,这个工作量将会非常大,因此可以选择设置一个唯一的类名进行优化

.only{

}

因此,尽量给每个标签打上类名,不要去通过父容器

当然,这样就需要耗费精力去取类名了

十九、尽量使用弹性布局

flexbox 性能会比之前的布局好, flexbox 之前的布局就有浮动,定位, flexbox 的性能是它们的四倍左右

最后

其实当被问到这个问题的时候,你内心应该是开心的,这个问题答案太多了,面试时间是有限的,你需要把握主动权

性能优化比较发散,可能你还清楚一些我不知道的手段,欢迎各位大佬进行补充

如果你对面试感兴趣,可以关注我的公众号:Dolphin海豚,可以加微信进面试群,讨论你面试过程中遇到的问题,我们一起解决

另外有不懂之处欢迎在评论区留言,如果觉得文章对你学习有所帮助,还请 ”点赞+评论+收藏“ 一键三连,感谢支持!