正则表达式

前言 正则表达式使用单个[字符串]来描述、匹配一系列匹配某个[句法规则]的字符串,通常被用来检索、替换那些符合某个模式(规则)的文本。 创建 1.构造函数创建 new RegExp(‘正则表达式’,‘修饰符’) var reg = new RegExp('hello','igm'); 2.直接直面量创建 /正则表达式/修饰符 var reg = /hello/igm; 字符分类 1.普通字符 字母、数字、下划线、汉字、没有特殊含义的符号(,;!@等) 实际上不是特殊字符的字符都是普通字符 2.特殊字符 \:将特殊字符转义成普通字符 3.模式修饰符 i:ignoreCase,匹配时忽视大小写 m:multiline,多行匹配 g:global,全局匹配 字面量创建正则时,模式修饰符写在一对反斜线后 实例方法 1.exec() 用来匹配字符串中符合正则表达式的字符串 如果匹配到,返回值是一个result数组: [匹配的内容,index: 在str中匹配的起始位置,input: 参数字符串,groups: undefined] 否则返回null var str = 'Hello world javascript hello'; var reg = new RegExp('hello','igm'); var res = reg.exec(str); console.log(res); 输出结果为: [ 'Hello', index: 0, input: 'hello world javascript hello', groups: undefined ] 2.test() 用来测试待检测的字符串中是否有可以匹配到正则表达式的字符串 如果有返回true,否则返回false var reg = /hello/; var str = 'hello world'; console.log(reg.test(str)); //true 3.toSting() toLocaleString() 把正则表达式的内容转化成字面量形式字符串/有本地特色的字符串 var reg = /hello/; var str = 'hello world'; console.log(reg.toString(),typeof reg.toString()); // /hello/ string console.log(reg.toLocaleString(),typeof reg.toLocaleString()); // /hello/ string 4.valueOf() 返回正则表达式本身 ...

October 1, 2024

static关键字详解

基础概念 static 是 C 语言定义的存储类说明符(Storage Class Specifier),Objective-C 和 Swift 中均有使用,但语义有所扩展。它的核心作用可以归纳为两点: 改变存储位置:将局部变量从栈区移到数据段,使其在函数返回后依然存活 限制链接可见性:将全局变量或函数的链接属性从 external 改为 internal,使其仅在当前编译单元(.m 文件)内可见 flowchart TD A["static 关键字"] --> B["修饰局部变量"] A --> C["修饰全局变量"] A --> D["修饰函数"] A --> E["Swift 中的 static"] B --> B1["存储位置:栈 → 数据段"] B --> B2["生命周期:函数作用域 → 整个进程"] B --> B3["作用域不变:仍限于函数内"] C --> C1["存储位置不变:仍在数据段"] C --> C2["链接属性:external → internal"] C --> C3["作用域缩小:全局 → 当前文件"] D --> D1["存储位置不变:仍在代码段"] D --> D2["链接属性:external → internal"] D --> D3["作用域缩小:全局 → 当前文件"] 一、静态局部变量 基本用法 在函数或方法内部用 static 修饰局部变量,使其只初始化一次,后续调用保留上次的值。 ...

May 3, 2026

原子化CSS

1. 什么是原子化CSS 1.1 基本概念 原子化CSS(Atomic CSS) 近年来热度逐渐增加,与原子化CSS相关的库在Github上也收获上万的Star。那么什么是原子化CSS呢,引用文章 Let’s Define Exactly What Atomic CSS is 中定义:“原子化CSS是一种CSS架构方式,其支持小型、单一用途的类,其名称基于视觉功能。” 更加通俗的来讲,原子化CSS是一种新的CSS编程思路,它倾向于创建小巧且单一用途的class,并且以视觉效果进行命名。举个简单的例子: <!-- 原子化类定义 --> <style> .text-white { color: white; } .bg-black { background-color: black; } .text-center { text-align: center; } </style> <!-- 原子化类使用 --> <div class="text-white bg-black text-center">hello Atomic CSS</div> 1.2 VS 行内样式 看到以上的示例,你可能很快就想到,直接使用行内样式不是更好吗,还省去了原子类的定义。这个问题可以从样式编写、一致性、功能、和缓存四个方面来回答。 在样式编写层面,CSS预处理和后处理器很大程度上依赖单独的样式表,原子化CSS可以充分利用Sass、Less等CSS预处理器功能进行样式的编写,同时可以借助PostCSS进一步增强CSS的功能。而对于行内样式,虽然在技术上支持使用预处理和后处理器对其进行处理,但很少有成熟的工具对此提供支持和维护。 在一致性层面,原子化CSS框架一般有预定义的设计系统,开发者仅能在设计系统中选择要设置的值。而对于行内样式或者传统CSS类定义来说,可设置的值是没有任何限制的。对于行内样式或者传统的CSS类设置来说,一个标签的字体大小可能是14px或0.875rem,当产品(or 客户)说需要调小一点时,开发者A可能调整为13px,开发者B可能调整为12px。但对于原子化CSS框架来说,调小一点意味着设置的类从text-sm变为text-xs。 以下为部分采用传统CSS类编写的网站样式统计数据(统计来源): 掘金官网:283种背景颜色 471种字体颜色 264种字体大小 GitLab:1199种背景颜色 1351种字体颜色 450种字体大小 CSDN:585种背景颜色 1190种字体颜色 504种字体大小 在功能方面,原子化CSS本质上还是CSS类,因此支持媒体查询功能,也支持对元素的悬停、聚焦等状态进行处理,而内联样式缺少这部分的能力。 在打包方面,内联样式包含在JS文件中,样式的修改会导致整个bundle的改变,原子化CSS样式定义和JS逻辑分离,修改元素的class属性可能并不影响(在没有新CSS类的情况下)最终打包输出样式文件。 2. 谁在使用原子化CSS 截至目前已经有部分网站借鉴或者使用原子化CSS的思想重构了自己的web网站,比较知名的网站有Facebook、Twitter、Github、swipperjs等。根据网上公开的信息,Facebook在使用原子化CSS思想重构之后,仅登录页面的413KB样式文件,减少为整个站点的74KB。 Facebook ...

November 9, 2024

输入 URL 到页面渲染完成

一、网站加载概述 面试过程中,常常遇到这样一道面试题,输入URL到页面加载完毕,浏览器做了哪些工作? 首先输入一个URL,你会看到浏览器上面的标签页出现了一个loading图标,开始时是逆时针旋转,接着顺时针旋转,当前页面消失,显示我们常说的空白页面,接着出现显示我们请求的新页面。此时如果网络很差,你有可能看到短暂的DOM页面,然后再看到渲染后的正常页面,这是从表面看到的加载过程,实际浏览器做的要多得多。 为什么浏览器这么多戏呢?直接显示不好吗,当然不行,就像喝粥,能直接吃米、喝水吗? 在用户输入URL到页面展示,浏览器要先向服务器获取前端资源,然后再将服务器返回的字节流转化成对应的页面,每一阶段都需要浏览器对应的能力进行处理的。 作为前端开发,了解整个过程其实很重要,只有知道了浏览器加载页面的整个过程,才能在开发中避免可能跳的坑,才能在发现问题后迅速定位问题,才能在性能优化时提出更多的解决方案。 比如下面问题,看了文章相信你就知道了为什么了 为什么会存在空白页面?(为了解决这个问题,各大厂都在实践各种方案) 为什么js要在Dom后面引入 为什么有个页面崩溃会导致多个页面崩溃 浏览器的缓存策略是怎样的? 为什么我们常常修改host就可以访问对应的域名 等等 二、浏览器的多进程架构 为了看懂下面的内容,这里必须要了解下现代浏览器的多进程架构,进程和线程的关系等知识点。 浏览器也是从单个进程架构一步步迭代到现代的多进程架构。 如上图,我们可以看到线程和进程的关系 通常一个程序实例就是一个进程,浏览器会为他分配内存空间。 一个进程间的数据是共享的,单线程就是一个进程包含一个线程,一个线程处理所有的任务 多线程就是,一个进程中包含多个线程可以同时执行任务,共享数据和内存 线程不能单独存在,必须依附于进程,一个线程失败,会导致进程执行失败,进程销毁,内存会被立即回收。 1、进程 下图是我们打开一个掘金首页后,再打开任务管理器,观察发现,此时浏览器包括的进程有很多,浏览器主进程,GPU进程,网络进程,存储进程,音频进程,渲染进程,多个插件进程。 这些进程负责的功能如下: 进程 说明 浏览器进程 负责浏览器各个子进程的通信,处理浏览器界面,包括地址栏等 渲染进程 也就是我们看到的图中的标签页进程,也就是我们常说的浏览器内核,v8就在这个进程。主要负责解析html、js、css渲染页面等 网络进程 负责发起网络请求,解析返回头信息 GUI进程 负责将渲染进程生成的图块转化成位图 渲染进程是运行在沙箱之中的,可以执行js,但是不能获取系统权限,和浏览器进程通过IPC通讯。这是为了保证浏览器进程的安全,锁进沙箱,即使有恶意代码,也不能突破沙箱读取或写入系统信息。 2、线程 通过上图,我们已经知道线程是不能独立存在的,必须依附于进程。这里我们主要关注下渲染进程,因为他的主要工作就是:完成页面的渲染和展示。 如图,我们可以看到渲染进程包括很多线程,除了图上展示的,还有合成线程等,各有各的作用,具体内容,我们将会在后面一章节,浏览器渲染原理分析中在来讨论。 三、浏览器请求的具体流程 从用户输入域名到浏览器渲染页面完成的过程,可以分为以下几个部分: 1、输入信息处理 2、网络请求 3、服务器返回请求资源 4、浏览器渲染 这里每一步涉及到的知识都非常多,其中缓存也在每个阶段占了很大比重,接着展开描述前三个阶段: 1、地址栏输入信息处理 当输入一个URL,浏览器会判断输入信息是检索信息还是请求URL 如果是检索的信息,就构建请求搜索的URL,调用浏览器默认的搜索引擎进行检索。 如果符合URL格式,浏览器主进程就通过IPC通信机制将URL发送给网络进程。 2、网络进程发起网络请求 1)网络进程首先会查找浏览器缓存,判断缓存是否存在,是否过期 如果存在且不过期,就直接返回缓存信息。具体的缓存策略可以接着看下面的浏览器缓存策略 2)如果没有缓存或过期,就开始进行DNS解析 DNS解析的过程也很复杂,最终目的就是拿到目标主机的IP地址,具体的解析过程可以看下面的域名解析 3)建立http连接或https连接 http通过三次握手建立连接 https需要建立TLS连接,浏览器会验证网站的数字证书是否合法,是否到期,是否安全等,这里https有自己的一套认证逻辑,我们重点不在这一块。 这里两个问题常被问到,不了解可以学习一下 http和https的区别? 简述三次握手、四次挥手? 4)发送请求 网络进程会构建http请求头,向服务器发送实际请求 5)网络传输,服务器处理,返回对应的资源 请求从应用层发出,经过运输层、网络层、物理层、数据链路层找到服务器,服务器拿到请求信息,返回对应的资源 这里的服务器可能是代理服务器,也可能是CDN节点,会判断当前数据是否存在缓存,如果有缓存且有效,直接返回(具体看配置的缓存策略),否则才会从服务器获取。 网络传输数据也是很复杂的过程,具体可以看下面简单的介绍计算机网络体系模型 3、服务器返回对应资源 1)处理返回信息 ...

November 5, 2024

跨域

一、什么是跨域? 在前端领域中,跨域是指浏览器允许向服务器发送跨域请求,从而克服Ajax只能同源使用的限制。 什么是同源策略? 同源策略是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。 同源策略限制以下几种行为: Cookie、LocalStorage 和 IndexDB 无法读取 DOM和JS对象无法获得 AJAX 请求不能发送 二、常见的跨域场景 URL 说明 是否允许通信 www.domain.com/a.js www.domain.com/b.js www.domain.com/lab/c.js 同一域名,不同文件或路径 允许 www.domain.com:8000/a.js www.domain.com/b.js 同一域名,不同端口 不允许 http://www.domain.com/a.js https://www.domain.com/b.js 同一域名,不同协议 不允许 http://www.domain.com/a.js http://192.168.4.12/b.js 域名和域名对应相同ip 不允许 http://www.domain.com/a.js http://x.domain.com/b.js http://domain.com/c.js 主域相同,子域不同 不允许 http://www.domain1.com/a.js http://www.domain2.com/b.js 不同域名 不允许 三、9种跨域解决方案 1、JSONP跨域 jsonp的原理就是利用<script>标签没有跨域限制,通过<script>标签src属性,发送带有callback参数的GET请求,服务端将接口返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback函数返回的数据。 1)原生JS实现: <script> var script = document.createElement('script'); script.type = 'text/javascript'; // 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数 script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback'; document.head.appendChild(script); // 回调执行函数 function handleCallback(res) { alert(JSON.stringify(res)); } </script> 服务端返回如下(返回时即执行全局函数): ...

October 1, 2024

weak详解

本文将深入探讨iOS中weak引用的实现原理,包括底层数据结构、核心函数实现、生命周期管理,以及weak、unowned、unsafe_unretained三者的对比。 weak的基本概念 weak是一种弱引用修饰符,它不会增加对象的引用计数,也就是说不会持有对象。当对象被释放时,所有指向该对象的weak引用会自动被置为nil,这是weak最核心的特性。 // Objective-C中使用weak @property (nonatomic, weak) id<SomeDelegate> delegate; __weak NSObject *weakObj = strongObj; // Swift中使用weak weak var delegate: SomeDelegate? weak var weakObj = strongObj weak的底层数据结构 要理解weak的实现原理,首先需要了解几个核心数据结构。 SideTable SideTable是Runtime中非常重要的数据结构。关于SideTable在引用计数存储中的作用,请参考iOS中的内存管理-侧表存储。 struct SideTable { os_unfair_lock slock; // 锁,保证线程安全 RefcountMap refcnts; // 引用计数哈希表 weak_table_t weak_table; // 弱引用表 }; 系统维护了一个固定大小的SideTable数组,称为StripedMap。通过对对象地址做哈希和取模来定位对应的SideTable: // 通过对象地址获取对应的SideTable static SideTable& table = SideTables()[obj]; // 内部等效逻辑:index = hash(obj) % StripeCount // StripeCount为StripedMap的大小 由于对象数量远大于SideTable数量,多个对象会被映射到同一个SideTable,这类似于哈希表中的哈希冲突。这种设计的核心目的是分散锁竞争——每个SideTable拥有独立的锁,不同SideTable上的操作可以并行执行,相比单一全局表大幅提升了多线程性能。 weak_table_t weak_table_t是存储弱引用关系的哈希表,采用 开放寻址法(线性探测) 解决哈希冲突: struct weak_table_t { weak_entry_t *weak_entries; // 连续分配的数组,作为开放寻址哈希表的底层存储 size_t num_entries; // 当前已使用的条目数量 uintptr_t mask; // 容量掩码(= 数组容量 - 1),用于 hash & mask 快速取模 uintptr_t max_hash_displacement; // 最大哈希冲突偏移量 }; 虽然 weak_entries 的类型是 weak_entry_t *(即一块连续内存),但元素不是按顺序填入的——插入时通过 hash(referent地址) & mask 计算目标槽位,冲突时向后线性探测。因此它本质上是一个用数组实现的开放寻址哈希表,而非普通的顺序数组。 ...

May 2, 2026

移动端适配

导读 移动端适配,是我们在开发中经常会遇到的,这里面可能会遇到非常多的问题: 1px问题 UI图完美适配方案 iPhoneX适配方案 横屏适配 高清屏图片模糊问题 … 上面这些问题可能我们在开发中已经知道如何解决,但是问题产生的原理,以及解决方案的原理可能会模糊不清。在解决这些问题的过程中,我们往往会遇到非常多的概念:像素、分辨率、PPI、DPI、DP、DIP、DPR、视口等等,你真的能分清这些概念的意义吗? 本文将从移动端适配的基础概念出发,探究移动端适配各种问题的解决方案和实现原理。 一、英寸 一般用英寸描述屏幕的物理大小,如电脑显示器的17、22,手机显示器的4.8、5.7等使用的单位都是英寸。 需要注意,上面的尺寸都是屏幕对角线的长度: 英寸(inch,缩写为in)在荷兰语中的本意是大拇指,一英寸就是指甲底部普通人拇指的宽度。 英寸和厘米的换算:1英寸 = 2.54 厘米 二、分辨率 2.1 像素 像素即一个小方块,它具有特定的位置和颜色。 图片、电子屏幕(手机、电脑)就是由无数个具有特定颜色和特定位置的小方块拼接而成。 像素可以作为图片或电子屏幕的最小组成单位。 下面我们使用sketch打开一张图片: 将这些图片放大即可看到这些像素点: 通常我们所说的分辨率有两种,屏幕分辨率和图像分辨率。 2.2 屏幕分辨率 屏幕分辨率指一个屏幕具体由多少个像素点组成。 下面是apple的官网上对手机分辨率的描述: iPhone XS Max 和 iPhone SE的分辨率分别为2688 x 1242和1136 x 640。这表示手机分别在垂直和水平上所具有的像素点数。 当然分辨率高不代表屏幕就清晰,屏幕的清晰程度还与尺寸有关。 2.3 图像分辨率 我们通常说的图片分辨率其实是指图片含有的像素数,比如一张图片的分辨率为800 x 400。这表示图片分别在垂直和水平上所具有的像素点数为800和400。 同一尺寸的图片,分辨率越高,图片越清晰。 2.4 PPI PPI(Pixel Per Inch):每英寸包括的像素数。 PPI可以用于描述屏幕的清晰度以及一张图片的质量。 使用PPI描述图片时,PPI越高,图片质量越高,使用PPI描述屏幕时,PPI越高,屏幕越清晰。 在上面描述手机分辨率的图片中,我们可以看到:iPhone XS Max 和 iPhone SE的PPI分别为458和326,这足以证明前者的屏幕更清晰。 由于手机尺寸为手机对角线的长度,我们通常使用如下的方法计算PPI: iPhone 6的PPI为 ...

November 9, 2024

前端存储

什么是前端存储 开门见山。前端存储就是通过前端技术来存储一段信息,然后在同源下的不同页面中都可以获取到已存储信息的一种策略。 前端存储的作用 方便网页的加载,避免了在发送请求收到响应前页面的空白期 可以在非强制性要求实时更新时减少向服务端的请求,加快渲染速度 在网络不佳或无网时仍有离线数据可以查看 有哪些前端存储方案 如图,一共有5种前端存储方案。大致可以分为3类: Cookie WebStorage:LocalStorage、SessionStorage 数据库存储:IndexedDB、WebSQL 先做一个简单对比: 下面我们详细介绍。 Cookie Cookie 的工作流程: Cookie 的构成: 域、路径、失效时间和安全性都是服务器给浏览器的指示,它们不会随着请求发送给服务器,发送给服务器的只有名称与值的键值对。 Cookie 的生命周期: 如果设定了 Cookie 的过期时间,那么 Cookie 会在到期时自动失效 如果没有设定过期时间,那么 Cookie 就是 session 级别的,即浏览器关闭时 Cookie 自动消失 Cookie 的优缺点: 优点: 可以控制过期时间,不会永久有效,有一定的安全保障 可进行扩展,可跨域共享 通过加密与安全传输技术,可以减少 Cookie 被破解的可能性 有较高的兼容性 缺点: 存储大小最多4KB 存储数量根据浏览器或浏览器版本的不同而不同,并且每个域最多20条 请求头上的数据容易被拦截攻击 存储的数据只能是字符串类型 操作 Cookie: // 设置 Cookie // Cookie 值必须是字符串类型,并且不支持分号、逗号以及空格, // 所以有时需要先使用 encodeURIComponent() 进行编码,或者使用 JSON.stringify() 进行序列化 document.cookie = '键1=值1;键2=值2;键n=值n'; // 读取 Cookie // 有时需要使用 decodeURIComponent() 或者 JSON.parse() document.cookie // 修改 Cookie // 如果键不存在,就新增;否则就修改 document.cookie = '已经存储过的键=新值'; // 删除 Cookie document.cookie = '要删除的键=任意值;max-age=0'; WebStorage WebStorage 是 HTML5 新加的,WebStorage(Web 存储) 分为 LocalStorage(本地存储) 和 SessionStorage(会话存储)。 ...

October 1, 2024

值类型和引用类型的区别

定义 值类型 值类型是指变量直接存储自身字段或描述信息的类型。值类型具有值语义,当将一个值类型变量赋值给另一个变量时,语义上会得到一份独立的值;底层是否立即复制完整数据,取决于编译器优化和写时拷贝等实现。 引用类型 引用类型是指变量存储的是指向数据在内存中位置的引用(指针)。当将一个引用类型变量赋值给另一个变量时,两个变量指向同一块内存区域。 Objective-C中的类型分类 在Objective-C中: 值类型:基础数据类型(int、float、double、BOOL、char等)、结构体(struct)、枚举(enum) 引用类型:除基础数据类型之外的大部分类型,包括NSObject及其子类(NSString、NSArray、NSDictionary、自定义类等) Swift中的类型分类 Swift对类型的分类更加清晰: 值类型:基础数据类型(Int、Float、Double、Bool等)、字符串(String)、结构体(Struct)、枚举(Enum)、数组(Array)、字典(Dictionary)、集合(Set)等 引用类型:类(Class)、闭包(Closure)、Actor等 拷贝机制差异 值类型 - 值语义拷贝 每个值类型变量都有自己的字段存储 对一个变量的操作不会影响另一个变量 赋值语义上会得到独立副本;如果字段是引用类型,复制的是引用本身,底层也可能通过写时拷贝延迟真正的数据复制 引用类型 - 浅拷贝 引用类型在内存中有一个指向该位置的引用 引用类型的变量可以指向相同类型的数据 对一个变量进行的操作会影响另一变量所指向的数据 内存分配机制 需要注意:值类型/引用类型描述的是语义模型,不等价于栈/堆分配规则。Swift编译器会根据生命周期、逃逸分析、优化级别、协议类型包装、集合存储等因素决定具体放在哪里。 理解Swift对象和字段的内存位置,可以先记住三个规则: class实例本体通常在堆上,通过引用计数管理生命周期 struct/enum的字段通常内联存储在这个值本身所在的位置 class类型的字段存储的是对象引用,也就是一个指针,真实class实例仍然在堆上 常见存储位置 局部值类型变量:生命周期明确、未逃逸时,通常可以放在当前线程的栈区,甚至被优化到寄存器中 class实例:实例本体在堆区,局部变量中保存的是指向堆对象的引用 class里的struct属性:struct属性内联存储在class实例这块堆内存中 struct里的class属性:struct内部只保存class引用,真实class实例仍然在堆上 被逃逸闭包捕获的值类型可能随闭包上下文一起存储在堆区 Array、Dictionary、Set、String等写时拷贝值类型通常只有一小段描述信息是值本身,真实元素或字符缓冲区可能在堆上 协议类型(existential container)持有大值类型时,如果超过存在容器的内联缓冲区大小,会使用堆分配 indirect enum的间接关联值会通过堆上的盒子存储,常见于递归枚举 全局变量、静态变量不属于栈区,也不是普通意义上的堆对象,它们通常位于全局/静态存储区 栈区内存分配和销毁通常只需移动栈顶指针,成本较低;堆区更动态,但分配、释放和引用计数维护都有额外成本。 struct中包含class属性 class Dog { var name: String init(name: String) { self.name = name } } struct Person { var age: Int var dog: Dog } var p1 = Person(age: 18, dog: Dog(name: "Lucky")) var p2 = p1 p2.age = 20 p2.dog.name = "Max" print(p1.age) // 18 print(p1.dog.name) // Max 内存关系可以近似理解为: ...

May 10, 2026

优化性能

前言 随着互联网发展至今,对于网站来说,性能显的越来越重要了,CSS作为页面渲染和内容展现的重要环节,影响着用户对整个网站的第一体验。所以,我们需要重视与CSS相关的性能优化。 项目开发初期我们可能因为各种原因(很大一部分原因是因为项目工期,产品往往把项目上线时间卡的死死的,根本不听你说的什么性能优化),怎么写的舒服就怎么来,对于性能优化我们常常在项目完成时才去考虑,经常被推迟到项目的末期,甚至到暴露出严重的性能问题时才进行性能优化。 为了更多地避免这一情况,首先要重视起性能优化相关的工作,将其贯穿到整个产品设计与开发中。其次,就是了解性能相关的内容,在项目开发过程中,自然而然地进行性能优化。 css渲染规则 想要优化CSS的性能,我们首先需要了解CSS的渲染规则,CSS选择器是从右向左进行匹配的 来看个例子🌰: .nav h3 a{font-size: 14px;} 渲染过程大概是:首先找到所有的a,沿着a的父元素查找h3,然后再沿着h3,查找.nav。中途找到了符合匹配规则的节点就加入结果集。如果找到根元素html都没有匹配,则不再遍历这条路径,从下一个a开始重复这个查找匹配(只要页面上有多个最右节点为a)。 Tips:为什么CSS选择器是从右向左匹配的? CSS中更多的选择器是不会匹配的,所以在考虑性能问题时,需要考虑的是如何在选择器不匹配时提升效率。从右向左匹配就是为了达成这一目的的,通过这一策略能够使得CSS选择器在不匹配的时候效率更高。这样想来,在匹配时多耗费一些性能也能够想的通了。 内联首屏关键CSS(Critical CSS) 性能优化中有一个重要的指标——首次有效绘制(First Meaningful Paint,简称FMP)即指页面的首要内容(primary content)出现在屏幕上的时间。这一指标影响用户看到页面前所需等待的时间,而 内联首屏关键CSS(即Critical CSS,可以称之为首屏关键CSS) 能减少这一时间。 很多人都喜欢通过link标签引用外部CSS文件。但需要知道的是,将CSS直接内联到HTML文档中能使CSS更快速地下载。而使用外部CSS文件时,需要在HTML文档下载完成后才知道所要引用的CSS文件,然后才下载它们。所以说,内联CSS能够使浏览器开始页面渲染的时间提前,因为在HTML下载完成之后就能渲染了。 但是我们不应该将所有的CSS都内联在HTML文档中,因为[初始拥塞窗口]存在限制(TCP相关概念,通常是 14.6kB,压缩后大小),如果内联CSS后的文件超出了这一限制,系统就需要在服务器和浏览器之间进行更多次的往返,这样并不能提前页面渲染时间。因此,我们应当只将渲染首屏内容所需的关键CSS内联到HTML中。 ⚠️还有一点需要注意的是内联CSS没有缓存,每次都会随HTML的加载而重新下载,但我们将内联首屏关键CSS控制在 14.6kB以内,它对性能优化还是起到正向作用的。(凡事有利也有弊) 异步加载非首屏CSS 我们需要知道两点内容:(具体可以看我之前的文章:这些浏览器面试题,看看你能回答几个?) CSS不会阻塞DOM的解析,但会阻塞DOM的渲染 CSS会阻塞JS执行,但不会阻塞JS文件的下载 由于CSS会阻塞DOM的渲染,所以我们将首屏关键CSS内联后,剩余的非首屏CSS内容可以使用外部CSS,并且异步加载,防止非首屏CSS内容阻塞页面的渲染。 CSS异步加载方式 第一种方法是动态创建 // 创建link标签 const myCSS = document.createElement( "link" ); myCSS.rel = "stylesheet"; myCSS.href = "mystyles.css"; // 插入到header的最后位置 document.head.insertBefore( myCSS, document.head.childNodes[ document.head.childNodes.length - 1 ].nextSibling ); 第二种方法是将link元素的media属性设置为用户浏览器不匹配的媒体类型(或媒体查询) 对浏览器来说,如果样式表不适用于当前媒体类型,其优先级会被放低,会在不阻塞页面渲染的情况下再进行下载。在首屏文件加载完成之后,将media的值设为screen或all,从而让浏览器开始解析CSS。 <link rel="stylesheet" href="mystyles.css" media="noexist" onload="this.media='all'"> 第三种方法是通过rel属性将link元素标记为alternate可选样式表 <link rel="alternate stylesheet" href="mystyles.css" onload="this.rel='stylesheet'"> 第四种方法是使用rel=preload来异步加载CSS <link rel="preload" href="mystyles.css" as="style" onload="this.rel='stylesheet'"> 注意,as是必须的。忽略as属性,或者错误的as属性会使preload等同于XHR请求,浏览器不知道加载的是什么内容,因此此类资源加载优先级会非常低。as的可选值可以参考上述标准文档。 看起来,rel="preload"的用法和上面两种没什么区别,都是通过更改某些属性,使得浏览器异步加载CSS文件但不解析,直到加载完成并将修改还原,然后开始解析。 但是它们之间其实有一个很重要的不同点,那就是使用preload,比使用不匹配的media方法能够更早地开始加载CSS。所以尽管这一标准的支持度还不完善,仍建议优先使用该方法。 CSS文件压缩 这应该是最容易想到的一个方法了,通过压缩CSS文件大小来提高页面加载速度。现在的构建工具,如webpack、gulp/grunt、rollup等也都支持CSS压缩功能。压缩后的文件能够明显减小,可以大大降低了浏览器的加载时间。 CSS层级嵌套最好不要超过3层 一般情况下,元素的嵌套层级不能超过3级,过度的嵌套会导致代码变得臃肿,沉余,复杂。导致css文件体积变大,造成性能浪费,影响渲染的速度!而且过于依赖HTML文档结构。这样的css样式,维护起来,极度麻烦,如果以后要修改样式,可能要使用!important覆盖。尽量保持简单,不要使用嵌套过多过于复杂的选择器。 删除无用CSS代码 一般情况下,会存在这两种无用的CSS代码:一种是不同元素或者其他情况下的重复代码,一种是整个页面内没有生效的CSS代码。 对于前者,在编写的代码时候,我们应该尽可能地提取公共类,减少重复。对于后者,在不同开发者进行代码维护的过程中,总会产生不再使用的CSS的代码,当然一个人编写时也有可能出现这一问题。而这些无用的CSS代码不仅会增加浏览器的下载量,还会增加浏览器的解析时间,这对性能来说是很大的消耗。所以我们需要找到并去除这些无用代码。 那么我们如何知道哪些CSS代码是无用代码呢? 谷歌的Chrome浏览器就有这种开箱即用的功能。只需转到查看>开发人员>开发人员工具,并在最近的版本中打开Sources选项卡,然后打开命令菜单。然后,点击Coverage,在Coverage analysis窗口中高亮显示当前页面上未使用的代码。 慎用*通配符 我们有时候可能会写下面这种代码来消除一些标签的默认样式或统一浏览器对标签渲染的差异化: *{ margin:0; padding:0; } 这样虽然代码量少,但它的性能可不是最佳的,我们最好还是写对应的标签选择器: ...

October 10, 2024