启用前端缓存
浏览器缓存
Cookie
-
可存储数据大小 4KB 左右
-
只能存字符串类型数据
-
Cookie 的属性
-
expires:Cookie 的过期时间,格式为 GMT 格式的日期字符串。如果不设置,Cookie 会在浏览器关闭时失效。
-
path:Cookie 的作用路径,默认为当前页面路径。
-
domain:Cookie 的作用域名。
-
HttpOnly:如果设置,浏览器将禁止通过 JavaScript 脚本读取或修改这些 Cookie。
-
secure:如果设置,Cookie 将仅通过 HTTPS 协议发送。
-
-
不同域名的网站之间默认不能互相访问对方的 Cookie,但可以通过设置 Cookie 的 domain 属性来实现跨域访问(仅限于主域相同子域不同的情况)。
localStorage
-
HTML5 新特性,可存储数据大小 5MB 左右
-
只能存字符串类型数据
-
API
-
新增/修改:localStorage.setItem('key', 'value')
-
读取:localStorage.getItem('key')
-
删除单个:localStorage.removeItem('key')
-
删除全部:localStorage.clear()
-
-
没有过期时间,关掉浏览器/重启电脑也会存在,除非手动清除缓存。但是可以通过封装方法来实现自定义过期时间,思路是在存储数据的同时,存储一个过期时间,并在每次读取数据时判断该数据是否过期。
-
严格遵循同源策略
sessionStorage
-
HTML5 新特性,可存储数据大小 5MB 左右
-
只能存字符串类型数据
-
API
-
新增/修改:sessionStorage.setItem('key', 'value')
-
读取:sessionStorage.getItem('key')
-
删除单个:sessionStorage.removeItem('key')
-
删除全部:sessionStorage.clear()
-
-
会话级别,只在会话期间有效。一旦标签页或窗口被关闭,存储的数据就会消失。
-
严格遵循同源策略
有了 localStorage 为什么还要使用 Cookie 呢?
-
Cookie 会自动随着每个 HTTP 请求发送到服务器,而 localStorage 不会。这一特性使得 Cookie 非常适合用于身份验证、会话跟踪和状态管理等场景。
-
Cookie 可以通过设置 HttpOnly 和 Secure 属性来提高安全性。
-
HttpOnly 可以用来防止跨站脚本(XSS)
-
Secure 设置仅在 HTTPS 协议上发送
-
-
Cookie 兼容性更好
-
服务器可以发送和修改 Cookie;而 localStorage 和 sessionStorage 完全由客户端控制,服务器无法直接访问或修改它们。
HTTP 缓存
是在 HTTP 协议中定义的一种数据缓存机制,通过在客户端(如浏览器)或代理服务器(如 nginx)中存储响应数据,以便在后续请求中复用这些数据
HTTP 缓存主要解决哪些问题?
-
减少不必要的网络传输
-
减低延迟、提高响应速度
-
减少服务器负载
-
可以离线预览
缺点就是会占用内存。
HTTP 缓存又分为两种缓存,强制缓存和协商缓存
强制缓存
如果浏览器判断请求的目标资源有效命中强缓存,则可以直接从内存中读取目标资源,无需与服务器做任何通讯。
Expires
在以前,我们通常会使用响应头的 Expires 字段去实现强缓存:
public class CacheControlServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 设置内容类型
response.setContentType("text/html;charset=UTF-8");
// 创建一个表示资源过期时间的Date对象
// 例如,设置资源在1小时后过期
Date expires = new Date(System.currentTimeMillis() + 1000 * 60 * 60); // 1小时后的时间
// 设置Expires头部
response.setDateHeader("Expires", expires.getTime());
// 你可以在这里添加更多的逻辑来生成响应内容
// 响应完成
}
// 注意:doPost等其他方法也可以根据需要被重写
}
Expires 判断强缓存是否过期的机制是:获取本地时间戳,与资源文件中的 Expires 字段的时间做比较,在时间范围内,则从内存(或磁盘)中读取缓存返回。
这里有一个巨大的漏洞:如果我本地时间不准咋办?
所以,Expires 字段几乎不被使用了。现在的项目中,我们使用 Cache-control 字段来代替 Expires 字段的强缓存功能。
Cache-control
Cache-control 是在资源的响应头上设置缓存时间,单位是秒。
response.setHeader("Cache-Control", "max-age=3600"); // 设置资源在1小时内有效
从第一次请求资源的时候开始,往后 N 秒内,资源若再次请求,则直接从内存(或磁盘)中读取,不与服务器做任何交互。
Cache-Control 有 6 个属性:
-
max-age:决定客户端资源被缓存多久。
-
s-maxag:决定代理服务器(如 nginx)缓存的时长。
-
no-cache:表示强制进行协商缓存,即跳过强缓存校验,直接去服务器进行协商缓存。
-
no-store:是表示禁止任何缓存策略。
-
public:表示资源即可以被浏览器缓存也可以被代理服务器缓存。
-
private:表示资源只能被浏览器缓存。
no-cache 和 no-store 互斥(即不能同时存在),public 和 private 互斥
Cache-Control 设置多个属性:
response.setHeader("Cache-Control", "max-age=10000,s-maxage=200000,public");
如果 Cache-Control 和 Expires 同时存在,Cache-Control 的优先级更高,会覆盖 Expires 的设置。
协商缓存
协商缓存主要有四个头字段,它们两两组合配合使用,Last-Modified 和 If-Modified-Since 一组,Etag 和 If-None-Match 一组,当同时存在的时候会以 Etag 和 If-None-Match 为主。
当命中协商缓存的时候,服务器 HTTP 状态码会返回 304,让客户端直接从本地缓存里面读取文件。
Last-Modified 和 If-Modified-Since
流程:
1、首次请求资源
1.1、浏览器发送请求:
浏览器首次向服务器请求某个资源(如 HTML、CSS、JS 文件等)
1.2、服务器响应请求:
-
服务器在返回资源的同时,在 HTTP 响应头中添加 Last-Modified 字段,该字段的值表示资源在服务器上的最后修改时间
-
浏览器接收资源并缓存起来,同时缓存响应头中的 Last-Modified 值
2、再次请求资源
2.1、浏览器检查缓存:
当浏览器再次请求相同的资源时,它会先检查本地缓存中是否存在该资源
2.2、构造请求头:
如果缓存资源存在且未过期且协商缓存被启用,浏览器会在 HTTP 请求头中添加 If-Modified-Since 字段,其值为上一次请求时服务器返回的 Last-Modified 值
2.3、发送请求:
浏览器将包含 If-Modified-Since 请求头的请求发送给服务器
3、服务器处理请求
3.1、检查资源修改时间:
-
服务器收到请求后,会检查请求头中的 If-Modified-Since 字段。
-
然后,服务器会将该字段值与资源在服务器上的当前最后修改时间做比较。
3.2、返回响应:
-
如果资源未修改(即服务器的最后修改时间未超过 If-Modified-Since 指定的时间):
-
服务器将返回 HTTP 状态码 304(Not Modified),并且不会返回资源内容。
-
浏览器接收到 304 状态码后,会从本地缓存中加载资源。
-
-
如果资源已修改:
-
服务器将返回 HTTP 状态码 200(OK),并发送资源的新内容。
-
同时,服务器还会在响应头中更新 Last-Modified 值,以便下次请求时使用。
-
4、浏览器更新缓存
-
如果服务器返回了 304 状态码,浏览器将保持本地缓存不变,并继续从缓存中加载资源。
-
如果服务器返回了 200 状态码和新的资源内容,浏览器将更新本地缓存中的资源文件和 Last-Modified 值。
这种方式的重点是判断资源文件的修改时间,以此来判断资源文件有没有被修改,而不是判断时间有效期。
这种方式的缺点是:
-
只要编辑了,不管内容是否真的有改变,都会更新最后修改时间。
-
Last-Modified 过期时间只能精确到秒。如果在同一秒既修改了文件又获取文件,客户端是获取不到最新文件的。
为了解决上述问题,从 HTTP.1 开始新增了一个头信息,ETag。
ETag 和 If-None-Match
ETag 就是将 Last-Modified 那套比较时间戳的形式修改成了比较文件指纹。
文件指纹就是根据文件内容计算出的唯一哈希值。文件内容一旦改变则指纹改变。
ETag 和 If-None-Match 流程与 Last-Modified 和 If-Modified-Since 流程几乎一样,只是将 Last-Modified 替换成 ETag,If-Modified-Since 替换成 If-None-Match,比较文件修改时间替换成比较文件指纹
ETag 有强验证和弱验证:
-
强验证:哈希码深入到每个字节,哪怕文件中只有一个字节改变了,也会生成不同的哈希值,它可以保证文件内容绝对的不变。
-
弱验证:提取文件的部分属性来生成哈希值,整体速度会比强验证快,但是准确率不高
ETag 缺点:
- 计算文件指纹意味着服务端需要更多的计算开销,文件尺寸大、数量多会影响服务器的性能,尤其是强验证会非常消耗计算量
很多网站在获取静态资源时会同时使用 Last-Modified 和 ETag,可能是考虑浏览器的兼容性问题吧。
区别
| 强制缓存 | 协商缓存 | |
|---|---|---|
| 工作原理 | 浏览器直接从本地缓存中读取资源,不向服务器发送请求 | 浏览器向服务器发送请求,询问资源是否更新,根据服务器响应决定是否使用缓存 |
| HTTP头字段 | 主要依赖 Cache-Control 或 Expires | 主要依赖 ETag 或 Last-Modified |
| 响应状态码 | 缓存命中时,无请求发出,因此无状态码 | 缓存命中时返回 304 Not Modified |
| 适用场景 | 适用于不经常变动的静态资源,如图片、CSS、JavaScript文件等 | 适用于可能被频繁更新的资源,如动态数据等 |
| 性能影响 | 减少网络请求,提高页面加载速度,降低服务器压力 | 仍然需要网络请求,但可以减少数据传输量,对于频繁更新的资源能确保用户获取最新内容 |
如果同时设置了强制缓存和协商缓存,浏览器会先判断强制缓存是否命中,如果强制缓存未命中,则再判断协商缓存是否命中。
使用 nginx 配置缓存
server {
listen 80;
server_name your-domain.com; # 修改为你的域名
location /vue-app/ {
alias /path/to/your/dist/; # 修改为你的Vue项目dist目录的实际路径
try_files $uri $uri/ /vue-app/index.html; # 对于单页面应用,确保所有路由都返回index.html
# 开启强制缓存
add_header Cache-Control "public, max-age=3000";
# 跳过强制缓存,强制进行协商缓存
# add_header Cache-Control "no-cache";
# 禁用缓存
# add_header Cache-Control "no-store";
# nginx 会自动给静态文件添加 Last-Modified 和 ETag 头部,无需额外配置
......
}
# 其他location块或server配置...
}
开启 GZIP 压缩
原理
通过将静态文件(js、css、图片等)压缩为 .gz 格式的文件,减小文件体积,从而提高文件加载速度。
使用
-
安装插件:
npm install compression-webpack-plugin -
配置 webpack(此过程省略)
-
打包(dist 中的静态文件转为 .gz 格式)
-
nginx 中配置
gzip_static on
节流和防抖
节流
频率极高的事件在固定时间内只触发一次。例如滚动条的滚动事件。
封装好的节流函数代码:
/*
@params (入参)
callback:需要节流的函数。 必传!
time:节流间隔时间点(也就是多久触发一次)不传的话默认是 300 毫秒
*/
const onScroll = (callback, time = 300) => {
let state = true; //触发判断条件
//判断如否有函数传入
if(typeof callback !== 'function'){
throw '第一个入参必须是函数,需要被节流的函数'
}
//制作一个闭包环境
return () => {
if(state){
callback();
state = false;
setTimeout(() => {
state = true;
}, time)
}
}
}
防抖
短时间内多次触发同一个事件时只执行一次。例如连续点击确定按钮只调一次方法。
封装好的防抖函数代码:
/*
@params (入参)
callback:需要节流的函数。 必传!
time:防抖间隔时间点(也就是倒计时触发的缓冲时间)不传的话默认是 300 毫秒
*/
const onchange = (callback, time = 300) => {
let asyncFun;
//判断需要被防抖的函数是否传入
if(typeof callback !== 'function'){
throw '第一个入参必须是函数,需要进行防抖的函数'
}
//创建一个闭包环境
return () => {
//在上一个函数被触发前,销毁他
if (asyncFun !== undefined) clearTimeout(asyncFun);
//创建一个新的函数
asyncFun = setTimeout(() => {
callback();
}, time)
}
}
减少重排和重绘
重排(也叫回流)和重绘是浏览器中相对比较耗时的动作。尤其是重排。
DOM 节点元素出现删除、增加、移动、尺寸改变的情况时,浏览器会先在指定位置上构建该元素的 DOM(重排),然后再对该元素进行渲染(重绘)。
重排一定会导致重绘,重绘不一定会引起重排。
重排的触发场景
-
删除或者新增一个节点元素
-
元素位置的改变,比如 float、position、overflow、display 等等
-
元素尺寸的改变,比如 margin、padding、height、width 等等
-
初始化构建 DOM 树的时候
-
窗口尺寸的变化,也就是 resize 事件发生的时候
-
填充内容的改变(内容撑大了某一个节点,内容改变,包含它的节点大小自然跟随调整。)
-
......
重绘触发场景
-
改变 background、color、border 等
-
visibility: hidden
-
css3 的 translate
-
border-style、border-radius、background-repeat、background-size、outline-color、text-decoration、box-shadow
-
......
图片优化
图片懒加载
核心思想在于延迟加载页面上的图片资源,直到这些资源即将出现在视口中时才开始加载。主要有两种实现方式:
1. <img loading="lazy">
HTML5 img 标签的新特性,用于开启图片延迟加载,直到图像即将进入视口才发送请求加载图像。
优点在于使用简单,但是在实际使用中往往不如预期,可控性较差,参考 图片延迟加载(懒加载)属性loading=‘lazy’实践
如果想要更加精细地控制图片懒加载,建议使用下面的方法。
2. data-src
HTML5 中我们可以使用 data-xxx 设置我们需要的自定义属性来进行一些数据的存放。前面的 data- 是固定的,后面的 xxx 一般为表示与自定义属性相关的字符串。img 标签中的 data-src 属性就属于一种自定义的 dataset 属性。
浏览器是否发起请求图片是根据 <img>的 src 属性,如果没有 src 属性,浏览器就不会发出请求去下载图片,或者把 src 属性设置成一张默认的加载效果图。所以懒加载基本的原理就是用 dataset 自定义属性取代 src 存储图片的路径,然后在检测到图片进入到可视区域的时候,再将其换为 src。
实现代码示例:
<script>
/*
window.innerHeight:获取窗口的高度 (不包括工具栏和滚动条)。
getBoundingClientRect():获取元素的左、上、右、下分别相对浏览器视窗的位置。
*/
function imgonload() {
let img = document.querySelectorAll("img");
/*console.log(img);*/
for(let i=0; i<img.length; i++) {
// 图片距离窗口上方的位置小于窗口的高度(也就是说该图片已经进入了窗口)
if(img[i].getBoundingClientRect().top < window.innerHeight) {
// 赋值
img[i].src = img[i].dataset.src;
}
}
}
function scollImg(fn) {
let timer = null;
let context = this;
return function () {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(context);
}, 500)
}
}
window.onload = imgonload;
// 绑定 scroll 事件,在滚动页面时触发
window.onscroll = scollImg(imgonload);
</script>
更简单的方法是使用插件:
// 安装插件
npm install vue-lazyload --save-dev
// main.js 引用
import VueLazyload from 'vue-lazyload';
Vue.use(VueLazyload);
// 使用
<img v-lazy="img/text.png"></a>
图片转 base64
Base64图片优势在于可以用文本的形式展示图片,也就是说不需要发起 HTTP 请求去下载图片资源。
格式如下:
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO 9TXL0Y4OHwAAAABJRU5ErkJggg==">
Base64图片的缺点:
-
文件体积会显著增加,因此不适合大型图片
-
无法使用缓存
如何将图片转为 base64 格式:
-
在 JavaScript 中可以使用 FileReader 方法来将图片转为 base64 格式
-
Webpack4 中配置
url-loader -
Webpack5 中配置
asset模块
使用 webp/svg 格式图片
-
webp
-
优点:体积相比 png/jpg 等格式的图片会更小。
-
缺点:兼容性较差。
-
-
svg
-
优点:可缩放性和高质量,简单图形体积更小。
-
缺点:复杂图形 svg 体积会变得相当大。
-
启用事件委托
路由懒加载
原本 webpack 在打包 vue 项目时会把所有路由组件打包进一个 JavaScript 文件中,可以通过使用 ES6 的动态加载模块 import() 函数,webpack 会将动态导入的路由单独打包成一个 JavaScript 文件,这样就可以实现按需加载,提高首页加载效率。
要实现路由懒加载,就得先将进行懒加载的子模块分离出来,打包成一个单独的文件,webpack 会自动帮我们做这件事。
const Home = () => import(/* webpackChunkName: "home" */ "@/views/home/index.vue");
const routes = [
{
path: "/",
name: "home",
component: Home
},
{
path: "/about",
name: "about",
component: ()=>import("@/views/about/index.vue")
},
{
path: "/user",
name: "user",
// webpack CommonJS 模块语法
component: resolve=>require(["@/views/user/index.vue"] , resolve) }
},
......
]
以上 3 种方法都可以实现路由懒加载。组件懒加载也是同理。
@ 符号的作用:
@ 符号通常被配置为一个别名(alias),用于简化模块路径的引用,是 webpack 的一个功能
// vue.config.js
chainWebpack: config => {
// key, value自行定义
config.resolve.alias
.set('@', resolve('src')) // 这里的 @ 代表 src 路径
.set('_c', resolve('src/components')) // 自定义 _c
.set('_conf', resolve('config')) // 自定义 _conf
}
Tree Shking
作用
消除无用的 JavaScript 代码,减少代码体积。
原理
ES6 模块的设计思想是尽量静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量,这就是 tree-shaking 的基础。
所以使用 CommonJS 模块语法时就不能进行树摇,因为它是运行时加载模块的。
"ES6 模块编译时就能确定模块的依赖关系" 这句话怎么理解?
当 ES6 遇到 import 语句时,它不会像 CommonJS 那样去执行模块,而是生成一个动态的只读引用。这意味着,直到实际需要的时候,ES6 模块才会去模块内部取值,而不是像 CommonJS 那样一次性加载整个模块。这种加载方式被称为"编译时加载"或"静态加载",它使得 ES6 模块在编译时就能完成模块的加载。
注意
webpack5 和 vue-cli 中已经默认开启了 tree-shaking,所以无需再配置,但是要注意代码中的一些写法会导致树摇功能失效,例如:
// util.js
export default {
test1(params) {
return params;
},
test2(params) {
return params;
}
};
// 引入并使用
import util from '../util';
util.test1(null)
示例中只使用了 test1() 未使用 test2(),但是打包后 test2() 依然会存在与打包文件中,树摇失效。
优化资源加载方式
-
script 正常模式
<script src="index.js"></script>-
有序同步下载,会阻塞 DOM 解析
-
将 script 放在 head 里:浏览器解析 HTML,发现 script 标签时,会先下载完所有这些 script 标签中的 JavaScript 文件,再往下解析其他的 HTML,会让网页内容呈现滞后,导致用户感觉到卡。
-
将 script 放在 body 最后:先解析完整个 HTML 页面,再下载 script 标签中的 JavaScript 文件。对于一些高度依赖于 JavaScript 的网页,就会显得慢了。
-
将 script 放在 body 外:不合标准。但是浏览器会忽略这个错误。
-
最优解是一边解析页面,一边下载 JavaScript 文件,所以有了下面的 2 种模式。
-
-
script async 模式
<script async src="index.js"></script>无序异步下载,不会阻塞 DOM 解析。
下载完成后会立即暂停 DOM 的解析(如果此时 DOM 还未完全解析完),然后立即执行脚本,脚本执行完成后,DOM 的解析会恢复。
-
script defer 模式
<script defer src="index.js"></script>有序异步下载,不会阻塞 DOM 解析。
无论下载何时完成,都会等待整个 DOM 解析完成后才执行。
下载的文件会在
DOMContentLoaded事件之前执行,且按照文件出现的先后顺序执行,一般情况下都可以使用 defer。 -
script module 模式
<script type="module">import { a } from './a.js'</script> -
link preload
<link rel="preload" as="script" href="index.js">-
用于提前加载一些需要的依赖,这些资源会优先加载。
-
preload 加载的资源是在浏览器渲染机制之前进行处理的,并且不会阻塞 onload 事件。
-
preload 加载的 JS 脚本其加载和执行的过程是分离的,即 preload 会预加载相应的脚本代码,待到需要时自行调用
-
-
link prefetch
<link rel="prefetch" as="script" href="index.js">-
该模式会在浏览器的空闲时候,加载一些未来可能会用到的资源
-
会将资源放入缓存至少5分钟
-
当页面跳转时,未完成的 prefetch 请求不会被中断
-
长列表虚拟滚动
只渲染可视区域的列表项,非可见区域不渲染,在滚动时动态更新可视区域。
插件:npm install vue-virtual-scroller
Web Worker 优化长任务
Web Worker 就是 JavaScript 中的多线程技术,允许主线程创建一个或多个 Worker 线程后台运行,等到 Worker 线程完成计算任务,再把结果返回给主线程,且这个过程中不会阻塞主线程。
Web Worker 的工作原理
-
创建 Worker 线程:主线程通过调用
new Worker(url)构造函数创建一个新的 Worker 线程,其中url是 Worker 线程将要执行的脚本文件的路径。 -
消息传递:主线程和 Worker 线程之间通过
postMessage()方法发送消息,并通过监听onmessage事件来接收消息。通信是双向的,但数据传递是拷贝的,不是共享的。 -
终止 Worker 线程:主线程可以通过调用 Worker 对象的
terminate()[ˈtɜːmɪneɪt] 方法来终止 Worker 线程。
示例代码:
// 主线程(main.js)
if (window.Worker) {
// 创建一个新的 Web Worker
// 注意:这里假设 worker.js 位于与 HTML 文件相同的目录下
const myWorker = new Worker('worker.js');
// 监听来自 Worker 的消息
myWorker.onmessage = function(e) {
console.log('Received message from worker: ', e.data);
};
// 向 Worker 发送消息
myWorker.postMessage('Hello, worker!');
// 当不再需要 Worker 时,可以终止它
// myWorker.terminate();
}
// Worker 线程(worker.js)
self.onmessage = function(e) {
console.log('Received message from main script: ', e.data);
// 执行一些耗时的操作
const result = doSomeHeavyProcessing(e.data);
// 将结果发送回主线程
self.postMessage(result);
};
function doSomeHeavyProcessing(data) {
// 假设这里进行一些复杂的计算
return `Processed ${data}`;
}
在 Web Worker 的上下文中,self.onmessage 是一个事件监听器,它用于监听来自创建它的主线程或其他 Worker 线程的消息。
self 关键字在 Web Worker 的上下文中是一个指向全局 Worker 对象的引用,与在浏览器主线程中使用的 window 对象类似。但是,在 Worker 中,window 对象是不可用的,因此使用 self 来代替。
self.onmessage = function(e) {
......
}
// 简化写法,效果相同
onmessage = function(e) {
......
};
Web Worker 中只能获取到部分浏览器提供的 API,如定时器、navigator、location、XMLHttpRequest(意味着可以使用 Ajax 请求) 等。
但是并不是所有的任务都适合开启 Web Worker,因为新建一个 Web Worker 时浏览器会加载对应的 worker.js 资源,这个过程会消耗时间,所以只有当任务的运算时间大于消耗时间才适合使用 Web Worker。
当面试官问你当页面处理10W条数据如何保证浏览器不卡顿时,你可以回答:使用 Web Worker。
骨架屏优化白屏
SPA 单页应用,无论 vue 还是 react,最初的 html 都是空白的,使用骨架屏,可以缩短白屏时间,提升用户体验。国内大多数的主流网站都使用了骨架屏,特别是手机端的项目。
骨架屏插件:npm i vue-skeleton-webpack-plugin