运行环境

Mr.ZhaoAbout 16 min

1. 页面加载

1. 浏览器加载资源的过程

  • 浏览器根据 DNS 服务器得到域名的 IP 地址
  • 向这个 IP 的机器发送 HTTP 请求
  • 服务器收到、处理并返回 HTTP请求
  • 浏览器得到返回内容

2. 浏览器渲染页面的过程

1. 基本过程

  1. 解析HTML的所有标签,深度遍历生成DOM树

  2. 解析CSS,构建层叠样式表模型CSSOM树

  3. 构建Render Tree(渲染树)

    DOM和CSSOM根据一定的规则组合起来生成了Render Tree

  4. 布局(Layout)

    确定各个元素的大小、位置。浏览器使用一种流式处理的方法,只需要一次绘制操作就可以布局所有的元素

  5. 绘制(Painting)

    浏览器会遍历Render Tree渲染树,调用paint方法,将渲染树的各个节点绘制到屏幕上

2. 关于CSS、JS阻塞问题

  • CSS 加载会不会阻塞 JS 的加载?(不会)
  • CSS 加载会不会阻塞 JS 的执行?(会)
  • CSS 加载会不会阻塞 DOM 的解析?(不会)
  • CSS 加载会不会阻塞 DOM 的渲染?(会)
  • JS 加载会不会阻塞 DOM 的解析?(会)
  • JS 加载会不会阻塞 DOM 的渲染?(会)
  • JS 执行会不会阻塞 DOM 的解析?(会)
  • JS 执行会不会阻塞 DOM 的渲染?(会)

关于 CSS,JS 的阻塞问题,都跟浏览器的渲染进程有关。而渲染进程又是多线程的

  • JS 引擎线程(单线程):负责解析 Javascript 脚本,运行代码
  • GUI 渲染线程:负责渲染浏览器界面,解析 HTML,CSS,构建 DOM Tree,CSSOM Tree 和 Render Tree,布局和绘制等

注意:GUI 渲染线程与 JS 引擎线程是互斥的,当 JS 引擎执行时 GUI 线程会被挂起,所以当 JS 加载和执行时,会阻塞住 DOM 的解析和渲染,导致白屏时间很长

DOM Tree 和 CSSOM Tree 是并行构建的,所以 CSS 加载不会阻塞 DOM 的解析;由于 Render Tree 是依赖于 DOM Tree 和 CSSOM Tree 的,因此,CSS 加载会阻塞 DOM 的渲染

GUI 渲染线程与 JS 引擎线程是互斥的,加载解析 CSS 时,JS 引擎会被挂起,所以 CSS 会阻塞 JS 的执行

  1. 如果遇到普通(sync)JavaScript脚本加载:文档解析的过程中,如果遇到JavaScript脚本,就会停止页面的解析进行下载,当脚本都执行完毕后,才会继续解析页面
  2. 如果遇到异步(async)JavaScript脚本加载:异步脚本会在HTML加载和解析完毕后执行
  3. 如果遇到设置了推迟的JavaScript脚本加载:文档解析时,遇到设置了defer的脚本,就会在后台进行下载,但是并不会阻止文档的渲染,当页面解析和渲染完毕后,会等到所有的defer脚本加载完毕并按照顺序执行完毕才会触发

defer是“渲染完再执行”:依赖于页面中的DOM元素(文档是否解析完毕),或者被其他脚本文件依赖

**async是“下载完就执行”:**并不关心页面中的DOM元素(文档是否解析完毕),并且也不会产生其他脚本需要的数据

2. 性能优化

1. 优化原则

  • 多使用内存、缓存或者其他方法
  • 减少 CPU 计算、减少网络加载耗时
  • 空间换时间

2. 实现方式

1. 加载资源优化

  • 静态资源的压缩合并(JS代码压缩合并、CSS代码压缩合并、雪碧图)
  • 静态资源缓存
  • 使用 CDN 让资源加载更快
  • 使用 SSR 服务器端渲染

2. 渲染优化

  • CSS 放在<head>里面, JS 放在<body>后面
  • 懒加载(图片懒加载)
  • 减少 DOM 查询,对 DOM 查询做缓存
  • 减少 DOM 操作,多个操作尽量合并在一起执行(DocumentFragment
  • 节流和防抖
  • 尽早执行操作(DOMContentLoaded

3. 详解

1. 静态资源的压缩合并

如果不合并,每个都会走一遍请求过程:

<script src="a.js"></script>
<script src="b.js"></script>
<script src="c.js"></script>

如果压缩了,就只走一遍请求过程:

<script src="abc.js"></script>

2. 静态资源缓存

通过链接名称控制缓存

<script src="abc_1.js"></script>

只有内容改变的时候,链接名称才会改变

<script src="abc_2.js"></script>

3. 使用 CDN 让资源加载更快

<script src="https://cdn.bootcss.com/zepto/1.0rc1/zepto.min.js"></script>

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

CDN的原理是尽可能的在各个地方分布机房缓存数据,这样即使我们的根服务器在国外,在国内的用户也可以通过国内的机房迅速加载资源

CDN就是根据用户位置分配最近的资源

通过CDN的负载均衡系统,智能调度边缘节点提供服务,相当于CDN服务的大脑,而缓存系统相当于CDN的心脏,缓存命中直接返回给用户,否则回源

作用:

  1. CDN有助于防御DDOS、MITM等网络攻击
  2. 减少了服务器负载
  3. 用户收到的内容来自最近的数据中心,延迟更低,内容加载更快

4. 使用 SSR 服务端渲染

SSR (server side render)服务端渲染,是指由服务侧(server side)完成页面的DOM结构拼接,然后发送到浏览器,为其绑定状态与事件,成为完全可交互页面的过程

5. 回流(重排)和重绘

回流必将引起重绘,重绘不一定会引起回流

1. 回流(重排)

当DOM结构中的元素改变尺寸、宽高、边框、内容、位置时,导致需要重新构建页面的过程

会导致回流的操作:

  • 增删可见的 DOM 元素时
  • 元素的尺寸或位置发生改变
  • 元素内容发生改变
  • 元素字体大小变化
  • 页面首次渲染
  • 浏览器窗口大小发生改变
  • 激活CSS伪类(例如:hover
  • 获取一些特定属性的值
    • offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight
    • 这些属性有一个共性,就是需要通过即时计算得到。因此浏览器为了获取这些值,也会进行回流
2. 重绘

当页面中元素的样式发生改变,但没有改变它在文档流中的位置时,导致浏览器重新绘制的过程

会导致重绘的操作:

  • 颜色的修改
  • 文本方向的修改
  • 阴影的修改

注意:当触发回流时,一定会触发重绘,但是重绘不一定会引发回流

3. 如何减少
  • 如果想设定元素的样式,通过改变元素的 class 类名 (尽可能在 DOM 树的最里层)
  • 避免设置多项内联样式
  • 应用元素的动画,使用 position 属性的 fixed 值或 absolute
  • 避免使用 table 布局,table 中每个元素的大小以及内容的改动,都会导致整个 table 的重新计算
  • 对于那些复杂的动画,对其设置 position: fixed/absolute,尽可能地使元素脱离文档流,从而减少对其他元素的影响
  • 使用CSS3硬件加速,可以让transformopacityfilters这些动画不会引起回流重绘
  • 避免使用 CSS 的 JavaScript 表达式

6. 懒加载

1. 什么是懒加载

懒加载也叫做延迟加载、按需加载,可视化区域之外的图片不会进行加载,在滚动屏幕时才加载,这样使得网页的加载速度更快,减少了服务器的负载。懒加载适用于图片(长列表)的场景中

2. 实现原理

图片的加载是由src引起的,当对src赋值时,浏览器就会请求图片资源。根据这个原理,我们使用HTML5 的data-xxx属性来储存图片的路径,在需要加载图片的时候,将data-xxx中图片的路径赋值给src,这样就实现了图片的按需加载,即懒加载

还可以给imgloading属性设为lazy

3. 代码实现

scroll版:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>懒加载</title>
        <style>
            /* 一定记得设置图片高度 */
            img {
                display: block;
                margin-bottom: 50px;
                height: 200px;
            }
        </style>
    </head>
    <body>
        <img src="./风景图/default.jpg" data-src="./风景图/01.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/02.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/03.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/04.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/05.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/06.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/07.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/08.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/09.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/10.jpg" />
        <script>
            function lazyLoad() {
                // 用属性选择器返回属性名为data-src的img元素列表
                let imgs = document.querySelectorAll('img[data-src]');
                imgs.forEach((item) => {
                    if (item.dataset.src === '') return;
                    if (isElementInViewport(item)) {
                        item.src = item.dataset.src;
                        item.removeAttribute('data-src'); //移除属性,下次不再遍历
                    }
                });
            }
            function isElementInViewport(element) {
                const rect = element.getBoundingClientRect(); // 用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置
                return (
                    rect.top >= 0 &&
                    rect.left >= 0 &&
                    rect.bottom <= (window.innerHeight ||
                            document.documentElement.clientHeight) &&
                    rect.right <=
                        (window.innerWidth ||
                            document.documentElement.clientWidth)
                );
            }
            lazyLoad(); //刚开始还没滚动屏幕时,要先触发一次函数,初始化首页的页面图片
            document.addEventListener('scroll', lazyLoad);
        </script>
    </body>
</html>

window.innerHeight 是浏览器可视区的高度

document.body.scrollTop || document.documentElement.scrollTop 是浏览器滚动的过的距离

imgs.offsetTop 是元素顶部距离文档顶部的高度(包括滚动条的距离)

图片加载条件:img.offsetTop <= window.innerHeight + document.body.scrollTop;

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>懒加载</title>
        <style>
            /* 一定记得设置图片高度 */
            img {
                display: block;
                margin-bottom: 50px;
                height: 200px;
            }
        </style>
    </head>
    <body>
        <img src="./风景图/default.jpg" data-src="./风景图/01.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/02.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/03.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/04.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/05.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/06.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/07.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/08.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/09.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/10.jpg" />
        <script>
            function lazyLoad() {
                // 用属性选择器返回属性名为data-src的img元素列表
                let imgs = document.querySelectorAll('img[data-src]');
                imgs.forEach((item) => {
                    if (item.dataset.src === '') return;
                    if (isElementInViewport(item)) {
                        item.src = item.dataset.src;
                        item.removeAttribute('data-src'); //移除属性,下次不再遍历
                    }
                });
            }
            function isElementInViewport(el) {
                // viewPortHeight 兼容所有浏览器写法
                const viewPortHeight =
                    window.innerHeight ||
                    document.documentElement.clientHeight ||
                    document.body.clientHeight;
                const offsetTop = el.offsetTop;
                const scrollTop = document.documentElement.scrollTop;
                return offsetTop <= viewPortHeight + scrollTop;
            }
            lazyLoad(); //刚开始还没滚动屏幕时,要先触发一次函数,初始化首页的页面图片
            document.addEventListener('scroll', lazyLoad);
        </script>
    </body>
</html>

IntersectionObserver版:

Intersection Observer 即重叠观察者,从这个命名就可以看出它用于判断两个元素是否重叠,因为不用进行事件的监听,性能方面相比getBoundingClientRect会好很多

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>懒加载</title>
        <style>
            /* 一定记得设置图片高度 */
            img {
                display: block;
                margin-bottom: 50px;
                height: 200px;
            }
        </style>
    </head>
    <body>
        <img src="./风景图/default.jpg" data-src="./风景图/01.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/02.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/03.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/04.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/05.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/06.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/07.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/08.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/09.jpg" />
        <img src="./风景图/default.jpg" data-src="./风景图/10.jpg" />
        <script>
            const images = document.querySelectorAll('img[data-src]');
            function handleIntersection(entries, observer) {
                entries.forEach((entry) => {
                    if (entry.isIntersecting) {
                        const image = entry.target;
                        image.src = image.dataset.src;
                        image.removeAttribute('data-src');
                        observer.unobserve(image);
                    }
                });
            }
            const observer = new IntersectionObserver(handleIntersection);
            images.forEach((image) => {
                observer.observe(image);
            });
        </script>
    </body>
</html>

7. DOM 查询做缓存

两段代码做一下对比:

// 不缓存 DOM 查询结果
for (let = 0; i < document.getElementsByTagName('p').length; i++); {
    // 每次循环,都会计算 length ,频繁进行 DOM 查询
}

// 缓存 DOM 查询结果
const pList = document.getElementsByTagName('p');
const length = pList.length;
for (let i = 0; i < length; i++) {
    // 缓存 length ,只进行一次 DOM 查询
}

总结:DOM 操作,无论查询还是修改,都是非常耗费性能的,尽量减少

8. 合并 DOM 插入

DOM 操作是非常耗费性能的,因此插入多个标签时,先插入 Fragment 然后再统一插入DOM

const listNode = document.getElementById('list');

// 创建一个文档片段,此时还没有插入到 DOM 树中
const frag = document.createDocumentFragment();

// 执行插入
for(let x = 0; x < 10; x++) {
    const li = document.createElement("li");
    li.innerHTML = "List item " + x;
    frag.appendChild(li);
}

// 都完成之后,再插入到 DOM 树中
listNode.appendChild(frag);

9. 尽早执行操作

window.addEventListener('load', function () {
    // 页面的全部资源加载完才会执行,包括图片、视频等
});
document.addEventListener('DOMContentLoaded', function () {
    // DOM 渲染完即可执行,此时图片、视频还可能没有加载完
});

10. 节流和防抖

详见:节流与防抖open in new window

3. web 攻击

1. XSS攻击

1. 什么是XSS攻击

XSS 攻击指的是跨站脚本攻击,是一种代码注入攻击。攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息如 cookie 等

XSS 的本质是因为网站没有对恶意代码进行过滤,与正常的代码混合在一起了,浏览器没有办法分辨哪些脚本是可信的,从而导致了恶意代码的执行

例如:我在一个博客网站正常发表一篇文章,输入汉字、英文和图片,完全没有问题。但是如果我写的是恶意的 js 脚本,例如获取到document.cookie然后传输到自己的服务器上,那我这篇博客的每一次浏览,都会执行这个脚本,都会把自己的 cookie 中的信息偷偷传递到我的服务器上来

2. 防御XSS攻击

  1. 使用HTTP Only Cookie:将敏感信息存储在HTTP Only Cookie中,这样浏览器脚本无法访问或修改这些Cookie。这可以有效防止XSS攻击者窃取用户的身份验证信息
  2. Content Security Policy(CSP):通过CSP设置一系列策略(即白名单),限制页面中可以执行的内容来源,包括脚本、样式和其他资源
  3. 输入验证:要对用户输入的数据进行验证和过滤,确保只接受预期格式和合法字符。使用服务器端验证来拒绝恶意脚本或特殊字符
  4. 输出转义:对需要插入到HTML中的代码进行转义

2. CSRF攻击

1. 什么是CSRF攻击

CSRF 攻击指的是跨站请求伪造攻击,攻击者诱导用户进入一个第三方网站,然后该网站向被攻击网站发送跨站请求。如果用户在被攻击网站中保存了登录状态,那么攻击者就可以利用这个登录状态,绕过后台的用户验证,冒充用户向服务器执行一些操作

CSRF 攻击的本质是利用 cookie 会在同源请求中携带发送给服务器的特点,以此来实现用户的冒充

必要条件:

  1. 登录受信任网站A,并在本地生成Cookie。(如果用户没有登录网站A,那么网站B在诱导的时候,请求网站Aapi接口时,会提示你登录)
  2. 在不登出A的情况下,访问危险网站B(其实是利用了网站A的漏洞)

2. 攻击类型

常见的 CSRF 攻击有三种:

  • GET 类型的 CSRF 攻击,比如在网站中的一个 img 标签里构建一个请求,当用户打开这个网站的时候就会自动发起提交
  • POST 类型的 CSRF 攻击,比如构建一个表单,然后隐藏它,当用户进入页面时,自动提交这个表单
  • 链接类型的 CSRF 攻击,比如在 a 标签的 href 属性里构建一个请求,然后诱导用户去点击

3. 防御CSRF攻击

  1. token验证
  2. 进行同源检测,根据HTTP请求头中的referer信息验证
  3. 对cookie进行双重验证
  4. 在设置cookie时设置samesite,限制cookie不可能作为第三方cookie使用

3. 中间人攻击

1. 什么是中间人攻击

中间人攻击 (Man-in-the-middle attack, MITM) 是指攻击者与通讯的两端分别创建独立的联系, 并交换其所收到的数据, 使通讯的两端认为他们正在通过一个私密的连接与对方直接对话, 但事实上整个会话都被攻击者完全控制。在中间人攻击中,攻击者可以拦截通讯双方的通话并插入新的内容

2. 如何防范

  • 使用安全信道
  • 使用加密通信

4. 可能引起前端安全问题的情况

  • 跨站脚本 (Cross-Site Scripting, XSS): ⼀种代码注入方式, 为了与 CSS 区分所以被称作 XSS。早期常见于网络论坛, 起因是网站没有对用户的输入进行严格的限制, 使得攻击者可以将脚本上传到帖子让其他人浏览到有恶意脚本的页面, 其注入方式很简单包括但不限于 JavaScript / CSS / Flash 等
  • iframe的滥用: iframe中的内容是由第三方来提供的,默认情况下他们不受控制,他们可以在iframe中运行JavaScirpt脚本、Flash插件、弹出对话框等等,这可能会破坏前端用户体验
  • 跨站点请求伪造(Cross-Site Request Forgeries,CSRF): 指攻击者通过设置好的陷阱,强制对已完成认证的用户进行非预期的个人信息或设定信息等某些状态更新,属于被动攻击
  • 恶意第三方库: 无论是后端服务器应用还是前端应用开发,绝大多数时候都是在借助开发框架和各种类库进行快速开发,一旦第三方库被植入恶意代码很容易引起安全问题