1 CSS和JS的位置

  • 考虑到HTML、CSS、JS三者存在互相堵塞的情况(详见“浏览器渲染原理”):

  • CSS建议放在head标签中

    • 由于CSS的下载与解析并不阻塞HTML的解析,所以尽早下载即可
    • 防止被JS阻塞(CSS优先,毕竟用户是先看到页面的)
    • 对于内联的JS,可以放在head标签中CSS之前
  • JS建议放在body标签的最后

    • 可以直接访问DOM,无需监听DOM Ready事件
    • 避免阻塞html的解析

2 白屏与闪烁问题

  • 如果CSS下载过慢,将导致页面白屏(页面空白)或闪烁(样式从无到有),根据浏览器不同以及link标签的位置不同,出现的结果也不相同。
    • 在Chrome浏览器中,无论link标签在head标签中还是在body标签的首部,都会出现白屏现象
    • 在FireFox浏览器中,如果link标签在head标签内,则出现白屏;如果link标签在body标签的上部,则出现闪烁
  • 实际上CSS并没有阻塞HTML的解析,但确实是阻塞的JS的下载与执行过程。

3 代码拆分技巧

  • 如果打包后仅生成一个js文件的话,当服务端做一个很小的改动,那么客户端就需要更新整个js文件,造成资源的浪费。

  • 因此建议将js代码根据js文件改动频率进行拆分:

    • runtime.js,该文件是webpack自己生成的代码,跟我们的代码隔离开比较好。假设我们不修改代码,只是将webpack升级,那么就只有runtime.js会改变

      1
      2
      3
      
      optimization: {
      	runtimeChunk: 'single',
      }
      
    • vendors.js,该文件包含全局使用的第三方基础库,比如React全家桶和Vue全家桶,一般来说我们不会去修改这部分代码,只会升级它们

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      
      optimization: {
      	splitChunks: {
      		cacheGroups: {
      			vendor: {
      				priority: 10,
      				minSize: 0, // 如果不写0,由于React文件尺寸太小,会直接跳过
      				test: /[\\/]node_modules[\\/]/, // 为了匹配/node_modules/或\node_modules\
              name: "vendors",
              chunks: 'all', // async表示异步加载,initial表示同步加载,all表示以上两者
              // 这三行的整体意思就是把两种加载方式的来自node_modules目录的文件打包为vendor.xxx.js
              // 其中vendor是第三方的意思
      			}
      		}
      	}
      }
      
    • common.js,该文件包含公司内部的基础库,比如公司内部的UI组件库,大概几个月会被更新或升级一次

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      
      optimization: {
      	splitChunks: {
      		cacheGroups: {
      			common: {
      				priority: 5,
      				minSize: 0,
      				minChunks: 2,// 如果一个模块被其他两个模块引用了,说明该模块是公共库
      				chunks: 'all',
      				name: 'common'
      			}
      		}
      	}
      }
      
    • page-index.js,该文件只包含当前页面的逻辑,每周都会变动。不同的页面有不同的index.js

      1
      2
      3
      4
      5
      6
      
      // 多页面可以通过配置多入口来做
      // 若文件过多,也可以单独遍历全部入口配置,再传入entry中
      entry: {
      	main: './src/index.js',
      	admin: './src/admin.js'
      }
      
  • CSS代码拆分也是一样的思路

4 JS动态导入

  • 原生js动态导入
1
2
3
import('库名').then((库实例) => {
	// 回调函数
})
  • Vue动态导入
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const Router = new VueRouter({
	routes: [
		{ path: '/home', component: () => import('./Home.vue') },
		{ path: '/about', component: () => ({ // 支持传入参数
			component: import('./About.vue'),
			loading: LoadingComponent,
			error: ErrorComponent
		})
	]
})
  • React动态导入
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 引入Suspense和lazy
import React, { Suspense, lazy } from 'react'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'

// 使用lazy去处理组件的引入
const Home = lazy(() => import('./routes/Home'))
const About = lazy(() => import('./routes/About'))

const App = () => (
	<Router>
    <!-- 要在Switch组件的外面包上Suspense -->
 		<Suspense fallback={LoadingComponent}>
 			<Switch>
 				<Route exact path="/" component={Home}/>
				<Route path="/about" component={About}/>
  		</Switch>
 		</Suspense>
 	</Router>
) 

5 懒加载

  • 懒加载就是一开始不加载,但需要用到的时候再加载。这听起来跟动态导入很像,不过懒加载一般指的是非JS资源,比如图片和样式等。

  • 常见的懒加载思路举例:

    1. 页面中有大量商品图片需要展示。假设代码为<img src='product.png'>
    2. 可以用一个1k大小的占位图片代替所有商品图片。代码改为<img src='placeholder.png' data-src='product.png'>。也就是说,创建一个自定义属性data-src存放真正需要显示的图片路径,而img自带的src放一张大小为1 * 1px的图片路径
    3. 在某个时刻(如页面加载的一秒钟后、用户滚动页面且快要看到下一页的产品时)使用JS去加载商品图片,替换掉占位图片。即当页面滚动直至此图片出现在可视区域时,用js取到该图片的data-src的值赋给src。

iShot2021-11-17 15.20.09

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
start()
$(window).on('scroll', function(){
 start()
})

function start(){
  //.not('[data-isLoaded]')选中已加载的图片不需要重新加载
 $('.container img').not('[data-isLoaded]').each(function(){
   var $node = $(this)
   if( isShow($node) ){
     loadImg($node)
   }
 })
}

//判断一个元素是不是出现在窗口(视野)
function isShow($node){
	return $node.offset().top <= $(window).height() + $(window).scrollTop()
}
//加载图片
 function loadImg($img){
  //.attr(值)
  //.attr(属性名称,值)
  $img.attr('src', $img.attr('data-src')) //把data-src的值 赋值给src
  $img.attr('data-isLoaded', 1)//已加载的图片做标记
}
  • 淘宝、天猫这类电商网站大量采用了这种方案,提速效果明显,而且可以为公司节省很多带宽成本。

6 预加载

  • 懒加载导致一些资源的加载被推迟,影响了用户体验。那么我们能不能把被推迟的资源提前下载下来呢?听起来很矛盾对不对,想象一下用户的操作:

    1. 打开淘宝,查看首屏
    2. 移动鼠标并点击,或者滚动鼠标滚轮
    3. 点看商品链接,或者查看第二屏
  • 第二步大概有一秒钟的时间,那么我们能不能在这个时候,预测用户的动作,并提前加载第三步的资源呢?答案是可以。

  • 比如,当用户的鼠标离某个链接还有200px时,我们提前用JS去get链接内容,那么就可以让用户提前看到内容了。(浏览器自带一个优化:不会在同一时间对同一个资源发两个请求,而是复用同一个请求。所以你不用担心有多余的请求。)代码如下:

1
2
3
4
5
6
$container.on('mousemove', e => {
  // 其他代码略
  if(距离链接小于200px){
  	axios.get('链接')
  }
} 
  • 比如,当用户屏幕滚动到距离图片还有200px时,我们提前用JS去get图片内容,那么等用户滚到第二屏时,说不定图片已经加载好了。代码如下
1
2
3
4
5
6
$container.on('wheel', e => {
  // 其他代码略
  if(举例图片小于200px){
  	axios.get('图片链接')
  }
} 
  • 除了预加载这些动态内容,程序员也可以预加载一些静态资源,写法如下:
1
2
3
<link rel="preload" href="page-2.css" as="style">
<link rel="preload" href="page-2.js" as="script">
<link rel="preload" href="video.mp4" as="video" type="video/mp4"> 

7 CSS代码优化技巧

  1. 使用uncss删掉无用的CSS,但这个方法无法保证不误删,所以实践中要谨慎使用,面试的时候可以提一下。
  2. 使用更高效的选择器
  3. 减少重排reflow)。在比较多种样式修改方案时,尽量选择不会引起重排的方案。比如在做动画时,修改transform永远比修改left、top、bottom、right更好,因为transform不会引起重排。
  4. 不要使用@import url.css;因为被加载的CSS不能与当前文件并行下载。实践中本来就没人用它,面试的时候提一下就行。
  5. 启用GPU硬件加速:在动画的元素上添加transform: translate3d(0, 0, 0);启用GPU加速渲染(默认是CPU计算并渲染的)

总的来说,CSS 能优化的空间并不大,而且节点数少于 2000 的页面也没有必要过早优化 CSS。

8 JS代码优化

JS代码的优化技巧主要指是不要使用那些「容易引起性能问题」的特性,比如

  • 尽量不用全局变量,因为全局变量太多会使变量查找变慢。

  • 尽量少操作DOM,可以使用Fragment一次性插入多个DOM节点。

  • 不要往页面中插入大量的HTML,一定会卡

  • 尽量少触发重排,可以使用节流和防抖来降低重排频率。

  • 尽量少用闭包,减少内存占用,避免内存泄漏(只有IE有内存泄露问题)。

  • 如果渲染10w条列表数据:用虚拟滚动列表(第三方库)

    • https://juejin.cn/post/6844904183582162957

    • 监听滚轮事件/触摸事件,记录列表的总偏移量。

    • 根据总偏移量计算列表的可视元素起始索引。

    • 从起始索引渲染元素至视口底部。

    • 当总偏移量更新时,重新渲染可视元素列表。

    • 为可视元素列表前后加入缓冲元素。

    • 在滚动量比较小时,直接修改可视元素列表的偏移量。

    • 在滚动量比较大时(比如拖动滚动条),会重新渲染整个列表。

    • 事件节流。