优秀的编程知识分享平台

网站首页 > 技术文章 正文

前端优化:懒加载思考(前端页面加载性能分析)

nanyue 2024-09-11 05:25:39 技术文章 5 ℃

杨帆,2018年加入去哪儿,很荣幸能和很多优秀的同事一起打拼,目前在大住宿/大前端/综合业务团队,主要负责专题系统的开发和优化等工作,涉及前端组件化开发和后端 node 工程的开发。希望在大前端架构思想上有所突破,帮助同事和团队在大前端领域里提高开发效率。

一、简介

在进行系统优化的时候,如果只局限于前端范畴的话,所能使用的方法很有限。通常能想到的就是如何让首屏更加流畅,提升用户体验。

懒加载指的是,合理的进行图片、静态资源延迟加载,甚至接口或业务逻辑的延迟执行,做到给首屏最大的性能空间,达到页面首屏更快的渲染效果。

这种思想,适合很多前端工程的优化场景。

二、实现思路

1、资源站位

在懒加载的时候,通常是需要跳过原本的执行步骤。比如:懒加载图片时,需要进行标签的站位,事先设定图片宽高,使用 data-* 属性设置好待请求的图片地址,这时,图片还没有被浏览器下载。

  1. <img
  2. class
  3. =
  4. "qLazy"
  5. data-original
  6. =
  7. 'https://xyz.abc.com/common/logo.png'
  8. />

由于没有此时没有设置 src 属性,所以 data-original 地址对应的图片并不会进行加载。

如果直接在 src 属性设置好图片地址,浏览器解析到则会直接下载,那么,就失去了懒加载的意义。

2、监听 scroll 事件

在懒加载时,需要制定懒加载的容器。比如:某些可以滚动的内容列表区域。

但在实际开发中,往往不会显式的制定容器,举个例子:手机端常常会有长列表,随整个页面滚动展现,这时如果进行懒加载优化,默认滚动容器应该为 window 对象。

3、判断资源位置是否在视窗内

这个功能是懒加载功能的核心,因为,懒加载也可以称为延迟加载,但什么时候进行加载呢?一定是用户在看到或者即将要看到待加载资源的时候,提前进行加载。这样,用户在看到资源时,资源已经加载完毕,可以给用户更好体验。

借用网络上一张图片加以说明:

即:当绿色待加载的页面元素,即将进入蓝色可视区域或已经进入可视区域后,执行懒加载方法。

4、判断加载资源或执行相应的回调逻辑

这个功能可以是根据业务进行扩展或更改。懒加载最初的构想只设计在了图片标签上,因为大部分手机端页面 80% 左右都会是图片,能够合理的控制图片的加载就能更好的改善用户体验。但也有例外,随着前端的功能越来越强大,很多渲染工作都会交给前端负责,很多业务逻辑也都又前端进行控制。所以,系统会很希望扩展一些懒执行功能,保证首屏逻辑优先完成,后续渲染或复杂操作延迟执行。

下面是部分懒加载实现源码。

  1. function
  2. QLazyLoader
  3. (
  4. setting
  5. )
  6. {
  7. var
  8. _setting
  9. =
  10. {
  11. selector
  12. :
  13. '.qLazy'
  14. ,
  15. // 事件名
  16. event
  17. :
  18. 'scroll'
  19. ,
  20. // 默认绑定 evnet 的对象容器
  21. container
  22. :
  23. setting
  24. .
  25. container
  26. ||
  27. window
  28. ,
  29. // 获取 src 的 data 属性,默认 data-attrbute
  30. attribute
  31. :
  32. 'data-original'
  33. ,
  34. // 设置懒加载类型 默认为img 选项: 图片:img 只执行回调:call
  35. loadtype
  36. :
  37. 'img'
  38. ,
  39. // 回调事件,元素出现在视口中 function
  40. appear
  41. :
  42. null
  43. ,
  44. // 触发 load 事件时执行的回调 function
  45. load
  46. :
  47. null
  48. };
  49. //省略:进行滚动监听
  50. //省略:判断是否出现再视窗内
  51. //如果元素出现在视窗内
  52. elements
  53. .
  54. each
  55. (
  56. function
  57. ()
  58. {
  59. var
  60. dom
  61. =
  62. this
  63. ;
  64. var
  65. jqThis
  66. =
  67. $
  68. (
  69. dom
  70. );
  71. var
  72. qSrc
  73. =
  74. jqThis
  75. .
  76. attr
  77. (
  78. setting
  79. .
  80. attribute
  81. );
  82. var
  83. loadAction
  84. =
  85. function
  86. ()
  87. {};
  88. var
  89. loadedCall
  90. =
  91. function
  92. ()
  93. {
  94. elements
  95. =
  96. $
  97. (
  98. grepElements
  99. (
  100. elements
  101. ));
  102. if
  103. (
  104. setting
  105. .
  106. load
  107. )
  108. {
  109. setting
  110. .
  111. load
  112. .
  113. call
  114. (
  115. dom
  116. ,
  117. jqThis
  118. ,
  119. elements
  120. .
  121. length
  122. ,
  123. setting
  124. );
  125. }
  126. }
  127. if
  128. (
  129. /img|background-image/
  130. .
  131. test
  132. (
  133. setting
  134. .
  135. loadtype
  136. )
  137. &&
  138. qSrc
  139. )
  140. {
  141. var
  142. _img
  143. =
  144. jqThis
  145. ;
  146. // 开始懒加载图片
  147. if
  148. (!
  149. _img
  150. .
  151. attr
  152. (
  153. 'src'
  154. ))
  155. {
  156. jqThis
  157. .
  158. attr
  159. (
  160. 'src'
  161. ,
  162. qSrc
  163. );
  164. }
  165. _img
  166. .
  167. one
  168. (
  169. 'error'
  170. ,
  171. function
  172. ()
  173. {
  174. console
  175. .
  176. log
  177. (
  178. 'qSrc loaded error'
  179. ,
  180. qSrc
  181. );
  182. });
  183. }
  184. if
  185. (
  186. /js/
  187. .
  188. test
  189. (
  190. setting
  191. .
  192. loadtype
  193. )
  194. &&
  195. qSrc
  196. )
  197. {
  198. loadAction
  199. =
  200. function
  201. ()
  202. {
  203. LoadScript
  204. (
  205. qSrc
  206. ,
  207. loadedCall
  208. );
  209. }
  210. }
  211. if
  212. (
  213. /css/
  214. .
  215. test
  216. (
  217. setting
  218. .
  219. loadtype
  220. )
  221. &&
  222. qSrc
  223. )
  224. {
  225. loadAction
  226. =
  227. function
  228. ()
  229. {
  230. LoadStyle
  231. (
  232. qSrc
  233. ,
  234. loadedCall
  235. );
  236. }
  237. }
  238. if
  239. (
  240. /call/
  241. .
  242. test
  243. (
  244. setting
  245. .
  246. loadtype
  247. ))
  248. {
  249. loadAction
  250. =
  251. loadedCall
  252. ;
  253. }
  254. jqThis
  255. .
  256. one
  257. (
  258. APPEAR_EVENT
  259. ,
  260. function
  261. (
  262. e
  263. )
  264. {
  265. if
  266. (!
  267. dom
  268. .
  269. loaded
  270. )
  271. {
  272. if
  273. (
  274. setting
  275. .
  276. appear
  277. )
  278. {
  279. setting
  280. .
  281. appear
  282. .
  283. call
  284. (
  285. dom
  286. ,
  287. $
  288. (
  289. this
  290. ),
  291. elements
  292. .
  293. length
  294. ,
  295. setting
  296. );
  297. }
  298. loadAction
  299. ();
  300. }
  301. });
  302. });
  303. }

三、如何判断资源位置是否出现在视窗内

1、根据可视区域高度和滚动高度以及元素距离页面顶端的距离进行计算

  1. function
  2. isElementInViewport
  3. (
  4. el
  5. )
  6. {
  7. // 获取div距离顶部的偏移量
  8. var
  9. offsetTop
  10. =
  11. el
  12. .
  13. offsetTop
  14. ;
  15. // 获取屏幕高度
  16. var
  17. clientHeight
  18. =
  19. document
  20. .
  21. documentElement
  22. .
  23. clientHeight
  24. ;
  25. // 屏幕卷去的高度
  26. var
  27. scrollTop
  28. =
  29. document
  30. .
  31. documentElement
  32. .
  33. scrollTop
  34. ;
  35. if
  36. (
  37. clientHeight
  38. +
  39. scrollTop
  40. >
  41. offsetTop
  42. )
  43. {
  44. console
  45. .
  46. log
  47. (
  48. "已经进入可视区"
  49. );
  50. }
  51. else
  52. {
  53. console
  54. .
  55. log
  56. (
  57. "并没有进入可视区"
  58. );
  59. }
  60. }

注意:document.documentElement 的兼容性问题,不在此次讲解范围内。

2、getBoundingClientRect API

这个方法非常有用,常用于确定元素相对于视口的位置。该方法会返回一个 DOMRect 对象,包含 left,top,width,height,bottom,right 六个属性。

left,right,top,bottom: 都是元素(不包括 margin )相对于视口的原点(视口的上边界和左边界)的距离。

  1. // 用法举例:
  2. var
  3. ro
  4. =
  5. object
  6. .
  7. getBoundingClientRect
  8. ();
  9. var
  10. Top
  11. =
  12. ro
  13. .
  14. top
  15. ;
  16. var
  17. Bottom
  18. =
  19. ro
  20. .
  21. bottom
  22. ;
  23. var
  24. Left
  25. =
  26. ro
  27. .
  28. left
  29. ;
  30. var
  31. Right
  32. =
  33. ro
  34. .
  35. right
  36. ;
  37. var
  38. Width
  39. =
  40. ro
  41. .
  42. width
  43. ||
  44. Right
  45. -
  46. Left
  47. ;
  48. var
  49. Height
  50. =
  51. ro
  52. .
  53. height
  54. ||
  55. Bottom
  56. -
  57. Top
  58. ;
  59. function
  60. isElementInViewport
  61. (
  62. el
  63. )
  64. {
  65. var
  66. rect
  67. =
  68. el
  69. .
  70. getBoundingClientRect
  71. ();
  72. return
  73. (
  74. rect
  75. .
  76. top
  77. >=
  78. 0
  79. &&
  80. rect
  81. .
  82. left
  83. >=
  84. 0
  85. &&
  86. rect
  87. .
  88. bottom
  89. <=
  90. (
  91. document
  92. .
  93. documentElement
  94. .
  95. clientWidth
  96. ||
  97. document
  98. .
  99. documentElement
  100. .
  101. clientHeight
  102. )
  103. &&
  104. rect
  105. .
  106. right
  107. <=
  108. (
  109. document
  110. .
  111. documentElement
  112. .
  113. clientWidth
  114. ||
  115. document
  116. .
  117. documentElement
  118. .
  119. clientWidth
  120. )
  121. );
  122. }

3、IntersectionObserver API

至于是否使用这个 API ,个人建议了解就好,目前仍处于 w3c 草案阶段 ,并且浏览器实现不太乐观。

这个 API 为开发者提供了一种可以异步监听目标元素与视窗 (viewport) 交叉状态的手段。

下面是一个懒加载模板内容的例子。

  1. function
  2. query
  3. (
  4. selector
  5. )
  6. {
  7. return
  8. Array
  9. .
  10. from
  11. (
  12. document
  13. .
  14. querySelectorAll
  15. (
  16. selector
  17. ));
  18. }
  19. var
  20. observer
  21. =
  22. new
  23. IntersectionObserver
  24. (
  25. function
  26. (
  27. changes
  28. )
  29. {
  30. changes
  31. .
  32. forEach
  33. (
  34. function
  35. (
  36. change
  37. )
  38. {
  39. var
  40. container
  41. =
  42. change
  43. .
  44. target
  45. ;
  46. var
  47. content
  48. =
  49. container
  50. .
  51. querySelector
  52. (
  53. 'template'
  54. ).
  55. content
  56. ;
  57. container
  58. .
  59. appendChild
  60. (
  61. content
  62. );
  63. observer
  64. .
  65. unobserve
  66. (
  67. container
  68. );
  69. });
  70. }
  71. );
  72. query
  73. (
  74. '.lazy-loaded'
  75. ).
  76. forEach
  77. (
  78. function
  79. (
  80. item
  81. )
  82. {
  83. observer
  84. .
  85. observe
  86. (
  87. item
  88. );
  89. });

这个 API 使懒加载变得简单了,由于不需要监听容器的滚动事件,全部由原生的观察者进行了异步回调。

四、扩展与优化

1、添加节流

scroll 事件,在滚动过程中,由于检测是否在视窗内的逻辑频繁被触发。因而频繁执行 DOM 操作、资源加载等浏览器负担消耗比较严重的行为。如果不加以控制很可能导致 UI 停顿甚至浏览器崩溃。

  1. // 添加节流控制。
  2. if
  3. (
  4. _setting
  5. .
  6. throttleMS
  7. >
  8. 0
  9. )
  10. {
  11. this
  12. .
  13. update
  14. =
  15. throttle
  16. (
  17. this
  18. .
  19. update
  20. ,
  21. _setting
  22. .
  23. throttleMS
  24. );
  25. }

2、添加仅垂直方向判断

通常懒加载情况 scroll 事件绑定在 window 对象,整个页面又是从上到下进行渲染,所以,横向的元素判断是否在视窗内通常没有意义。设置开关,可以减少横向判断逻辑,减少资源消耗。

  1. // 只判断垂直方向元素是否出现在视窗内
  2. if
  3. (
  4. setting
  5. .
  6. vertical
  7. )
  8. {
  9. if
  10. (
  11. jqThis
  12. .
  13. height
  14. ()
  15. <=
  16. 0
  17. ||
  18. jqThis
  19. .
  20. css
  21. (
  22. 'display'
  23. )
  24. ===
  25. 'none'
  26. ){
  27. return
  28. ;
  29. }
  30. if
  31. (
  32. $
  33. .
  34. abovethetop
  35. (
  36. this
  37. ,
  38. setting
  39. ))
  40. {
  41. // Nothing.
  42. }
  43. else
  44. if
  45. (!
  46. $
  47. .
  48. belowthefold
  49. (
  50. this
  51. ,
  52. setting
  53. ))
  54. {
  55. jqThis
  56. .
  57. trigger
  58. (
  59. APPEAR_EVENT
  60. );
  61. counter
  62. =
  63. 0
  64. ;
  65. }
  66. else
  67. {
  68. if
  69. (++
  70. counter
  71. >
  72. setting
  73. .
  74. failureLimit
  75. )
  76. {
  77. return
  78. false
  79. ;
  80. }
  81. }
  82. }

3、扩展功能

前面可能说过,懒加载并不局限与图片情况,有时根据业务需求可以扩展需要延迟加载的静态资源。比如:js 外链脚本、css 外链样式、视频、音频等。或者只放出回调,后续具体业务逻辑交由开发自行处理。

具体思路,可以在初始化时,配置指定参数决定当前懒加载实例执行哪种功能加载。也可以在站位 DOM 中配置属性进行懒加载类型判断,可以根据业务需要灵活处理。

五、总结

懒加载对于前端优化来讲,可以称得上是一种万金油的优化方式,很多场景都可以使用其提升页面体验。

但懒加载核心是判断元素是否在视窗内的操作,这种判断是要在视窗滚动过程中获取元素宽高,导致页面会频繁重绘,会耗费很大的性能开销。

而且在监听 DOM 方面,如果懒加载的逻辑是延迟渲染某段 HTML 代码的话,那么在绑定 DOM 事件方面也是有更多详细考虑才行,否则可能会出现监听事件失效的问题。

所以,在应用懒加载的场景,还是要多多考虑,如果页面资源较少,页面整体逻辑较简单,可以不使用懒加载,一次性加载完成体验可能还会更优于懒加载优化效果。

希望大家可以在工作中合理运用懒加载思想,做到页面的更好体验。

Tags:

最近发表
标签列表