研究表明,更好的核心網頁指標可以提高用戶互動度和業務指標。例如:
每項核心 Web 指標代表用戶體驗的一個不同方面,能夠進行實際測量,并且反映出以用戶為中心的關鍵結果的真實體驗。核心 Web 指標的構成指標會隨著時間的推移而發展 。當前針對 2020 年的指標構成側重于用戶體驗的三個方面——加載性能、交互性和視覺穩定性——并包括以下指標(及各指標相應的閾值):
本文將主要討論復雜頁面的加載性能—— Largest Contentful Paint (LCP)。
以下示例展示了一些熱門網站上出現最大內容繪制的時間點:
在上方的兩個時間軸中,最大元素隨內容加載而變化。在第一個示例中,新內容被添加進 DOM,并因此使最大元素發生了改變。在第二個示例中,由于布局的改變,先前的最大內容從可視區域中被移除。
雖然延遲加載的內容通常比頁面上已有的內容更大,但實際情況并非一定如此。接下來的兩個示例顯示了在頁面完全加載之前出現的最大內容繪制。
在第一個示例中,Instagram 標志加載得相對較早,即使其他內容隨后陸續顯示,但標志始終是最大元素。在 Google 搜索結果頁面示例中,最大元素是一段文本,這段文本在所有圖像或標志完成加載之前就顯示了出來。由于所有單個圖像都小于這段文字,因此這段文字在整個加載過程中始終是最大元素。
根據當前最大內容繪制 API中的規定,最大內容繪制考量的元素類型為:
本文主要討論復雜頁面的加載性能度量,相對于簡單的頁面加載性能,可以直接使用Google Performance,如下圖。
對于特殊頁面的交互是無法通過使用Performance獲取相關指標數據,如下圖,獲取 PageOffice彈窗插件中的文本內容加載時間。
本文主要討論這種復雜場景加載性能的計算方式。
計算的指標同樣參考Largest Contentful Paint (LCP) 指標,具體計算原理為:
將視頻進行逐幀分析,首先計算最大元素出現的第一幀,同時計算出“開始加載的頁面標識元素”所出現的最后一幀,然后計算出幀數差,最后使用幀差數除以視頻幀率,得出加載時間。
具體實現邏輯
步驟一: 將采用將視頻進行逐幀分析,計算最大元素出現的第一幀,再通過“開始加載的頁面標識元素”所出現的最后一幀,如下圖,折線圖為最大元素,最大元素出現的第一幀就是我們要找出的幀;圖中分析按鈕為“開始加載的頁面標識元素”,當點擊按鈕后,按鈕消失前的最后一幀就是我們要找出的幀。
步驟二:計算出幀數差,“開始加載的頁面標識元素”出現的最后一幀的幀數 與 最大元素出現的第一幀的幀數之差,這就是頁面加載過程的總幀數。
步驟三:幀數差除以視頻幀率(每一秒包含的幀數),最終計算出加載時間。
Python實現偽代碼
# -*- coding: cp936 -*-
import cv2, os, time
from config.deployment_config import END_IMAGE_PATH, IMAGE_SAVE_PATH, IMAGE_SAVE_AS_PATH, \
START_IMAGE_PATH
from lib.ui_image_identification.core.image_identification import ImageIdentification
from lib.ui_image_identification.uicv import imread
class FrameTimeDiff:
def __init__(self, mp4):
# 視頻路徑,直接把腳本和視頻放在同一個目錄下最好,也可以指定對應的視頻路徑
self.cap = cv2.VideoCapture(mp4)
(major_ver, minor_ver, subminor_ver) = (cv2.__version__).split('.')
if int(major_ver) < 3:
self.fps = self.cap.get(cv2.cv.CV_CAP_PROP_FPS)
else:
self.fps = self.cap.get(cv2.CAP_PROP_FPS)
def extract_picture_frame(self):
"""
提取視頻中的圖片幀
"""
i = 0
while True:
ret, frame = self.cap.read() # ret:True或者False,代表有沒有讀取到圖片;frame:表示截取到一幀的圖片
if not ret:
break
# 展示圖片
cv2.imshow('capture', frame)
image_path = f".\image\\{i}.png"
# 保存圖片
cv2.imwrite(image_path, frame)
i = i + 1
if cv2.waitKey(1) & 0xFF == ord('q'):
break
@staticmethod
def merge_number(num_lst):
"""
合并臨近數,方便計算“開始加載的頁面標識元素”出現的最后一幀的幀數 與 最大元素出現的第一幀的幀數
eg: [1, 3,4,5,6, 9,10] -> [[1], [3, 4, 5, 6], [9, 10]]
"""
num_lst_tmp = [int(n) for n in num_lst]
sort_lst = sorted(num_lst_tmp) # ascending
len_lst = len(sort_lst)
i = 0
split_lst = []
tmp_lst = [sort_lst[i]]
while True:
if i + 1 == len_lst:
break
next_n = sort_lst[i + 1]
if sort_lst[i] + 2 == next_n:
tmp_lst.append(next_n)
else:
split_lst.append(tmp_lst)
tmp_lst = [next_n]
i += 1
split_lst.append(tmp_lst)
return split_lst
@staticmethod
def extract_key_frame(target_pos, _filter_image_list=None, image_save_path=IMAGE_SAVE_PATH):
圖片幀檢測標識圖,如最大元素截圖
__list = []
png_list = {
int(os.path.splitext(yml)[0]): os.path.join(image_save_path, yml)
for root, dirs, files in os.walk(image_save_path)
for yml in files
if os.path.splitext(yml)[-1] == ".png"
}
if _filter_image_list is None:
_filter_image_list = []
for index in sorted(png_list):
if int(index % 2) == 0:
file_path = png_list[index]
print(file_path)
# 圖像識別定位
if index not in _filter_image_list:
# 在當前圖片幀中檢測是否存在標識圖片
last_scroll_location, last_scroll_match_result, last_scroll_best_match_detail = \
ImageIdentification(
target_pos,
threshold=0.99,
resolution=(1920, 1080)
).match_in(imread(file_path))
if last_scroll_location is not None:
print(file_path, last_scroll_location)
image_path = os.path.join(IMAGE_SAVE_AS_PATH, f"{index}.png")
frame = imread(file_path)
# 保存圖片
cv2.imwrite(image_path, frame)
__list.append(index)
_filter_image_list.append(index)
return __list, _filter_image_list
def analysis_more_picture_frame(self):
"""
計算多次重復頁面操作的加載時間
"""
find_1_list, filter_image_list = FrameTimeDiff.extract_key_frame(target_pos=START_IMAGE_PATH)
find_2_list, filter_image_list = FrameTimeDiff.extract_key_frame(
_filter_image_list=filter_image_list,
target_pos=END_IMAGE_PATH
)
# “開始加載的頁面標識元素”出現的幀
merge_list_1 = FrameTimeDiff.merge_number(find_1_list)
# 最大元素出現的幀
merge_list_2 = FrameTimeDiff.merge_number(find_2_list)
sectionalization = len(merge_list_1)
time_difference_list = []
for i in range(sectionalization):
start_frame_index = merge_list_1[i][-1]
end_frame_index = merge_list_2[i][0]
time_difference = (end_frame_index - start_frame_index) / self.fps
time_difference_list.append({"time_difference": time_difference, "start_frame_index": start_frame_index,
"end_frame_index": end_frame_index})
return time_difference_list
if __name__ == '__main__':
frameTimeDiff = FrameTimeDiff("20230703-164844.mp4")
# 提取視頻中的圖片幀
frameTimeDiff.extract_picture_frame()
# 釋放對象和銷毀窗口
frameTimeDiff.cap.release()
cv2.destroyAllWindows()
# 計算頁面操作的加載時間
time_diff = frameTimeDiff.analysis_more_picture_frame()
print(time_diff)
考慮到偽代碼易讀性,未添加多進程處理邏輯。上述 ImageIdentification 方法是封裝的圖像識別方法,封裝了kaze、brisk、sift、surf等圖像識別算法。
通過 Selenium 實現頁面的操作自動化執行,通過自動錄屏,獲取操作過程的視頻。最后通過上述 LCP指標的計算方式,是可以實現復雜頁面加載時間測試的自動化測試的,有興趣的可以嘗試一下。
imeDown.js
/*
時間倒計時插件
TimeDown.js
*/
function TimeDown(id, endDateStr) {
//結束時間
var endDate = new Date(endDateStr);
//當前時間
var nowDate = new Date();
//相差的總秒數
var totalSeconds = parseInt((endDate - nowDate) / 1000);
//天數
var days = Math.floor(totalSeconds / (60 * 60 * 24));
//取模(余數)
var modulo = totalSeconds % (60 * 60 * 24);
//小時數
var hours = Math.floor(modulo / (60 * 60));
modulo = modulo % (60 * 60);
//分鐘
var minutes = Math.floor(modulo / 60);
//秒
var seconds = modulo % 60;
//輸出到頁面
document.getElementById(id).innerHTML = "還剩:" + days + "天" + hours + "小時" + minutes + "分鐘" + seconds + "秒";
//延遲一秒執行自己
setTimeout(function () {
TimeDown(id, endDateStr);
}, 1000)
}
html
<!DOCTYPE html>
<html>
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>時間倒計時</title>
<script src="TimeDown.js"></script>
</head>
<body>
<form id="form1" runat="server">
<div id="show">
</div>
<script type="text/javascript">
TimeDown("show", "2024-03-9 8:00:45");
</script>
</form>
</body>
</html>
顯示效果:
還剩:2天19小時29分鐘5秒
關于setTimeout與setInterval的區別:
setTimeout只會執行一次, 在執行完成后, 重新啟動新的Timeout, 時間runtime計算設置為差時, 減少出現間隔越來越大的情況; 而setInterval()會導致間隔越來越大的情況, 而出現執行時間不準確的問題:
1 Javascript會把執行的回調函數、瀏覽器的觸發事件、UI渲染事件,先放到隊列中, 隊列根據先進先出的規則, 依次執行他們, 當執行到隊列中的setInterval時很難保證其與setTimeout同步關系還保持。
2 setInterval無視代碼錯誤:代碼報錯, 但是setInterval依舊會按時執行, 不會中斷。
3 setInterval無視網絡延遲:如果調用ajax或其他服務, 他不會管是否返回回調, 會繼續按時執行。
4 setInterval不保證執行:因為setInterval會定時執行, 如果函數邏輯很長, 間隔時間內執行不完, 后續方法會被拋棄。
5 setInterval會受瀏覽器狀態影響、最小化、最大化、tab切換等外界因素的影響。
網:https://day.js.org/en
Github:https://github.com/iamkun/dayjs
1. 安裝
1.1. 下載
下載地址:https://github.com/iamkun/dayjs/releases
<script src="path/to/dayjs/dayjs.min.js"></script>
<script>dayjs().format()</script>
1.2. CDN
<script src="https://unpkg.com/dayjs@1.8.21/dayjs.min.js"></script>
<script>dayjs().format()</script>
1.3. Node.JS
npm install dayjs --save
var dayjs = require('dayjs')
//import dayjs from 'dayjs' // ES 2015
dayjs().format()
1.4. TypeScript
npm install dayjs --save
import * as dayjs from 'dayjs'
dayjs().format()
更多見官方文檔:https://day.js.org/docs/en/installation/typescript
2. API
2.1. 解析
Day.js 并沒有改變或覆蓋 Javascript 原生的 Date.prototype, 而是創造了一個全新的包含 Javascript Date 的 Day.js 對象,可以直接使用 dayjs() 來調用。
Day.js 對象是不可變的, 所有的 API 操作都將返回一個新的 Day.js 對象。
// 返回包含當前日期和時間的 Day.js 對象
// 什么都不傳,相當于 dayjs(new Date())
let now = dayjs();
// 傳入一個標準的 ISO 8601 時間字符串
// https://en.wikipedia.org/wiki/ISO_8601
let date = dayjs('2020-06-01');
// 傳入一個 Unix 時間戳 (13位)
let date = dayjs(1591149248030);
// 傳入一個 Javascript Date 對象
let date = dayjs(new Date(2020, 6, 1));
// 因為 Day.js 對象是不可變的,可使用如下方法獲取一個對象拷貝
let date1 = date.clone(); // 方法一:在一個 Day.js 對象上使用 clone 函數
let date2 = dayjs(date); // 方法二:傳入一個 Day.js 對象
// 檢查當前 Day.js 對象是否是有效日期時間
if (dayjs().isValid()) {
// 有效
} else {
// 無效
}
2.2. 獲取和設置
// 獲取,返回 number 類型的值
dayjs().year(); // 年 ==> dayjs().get('year')
dayjs().month(); // 月 ==> dayjs().get('month')
dayjs().date(); // 日 ==> dayjs().get('date')
dayjs().hour(); // 時 ==> dayjs().get('hour')
dayjs().minute(); // 分 ==> dayjs().get('minute')
dayjs().second(); // 秒 ==> dayjs().get('second')
dayjs().millisecond(); // 毫秒 ==> dayjs().get('millisecond')
dayjs().day(); // 本周的第幾天 ==> dayjs().get('day')
// 設置,單位對應的值大小寫不敏感
dayjs().set('month', 3);
dayjs().set('second', 30);
2.3. 操作
// 增加
dayjs().add(7, 'day'); // 增加 7 天
// 減少
dayjs().subtract(2, 'month'); // 減少 2 個月
// 開頭
dayjs().startOf('month'); // 當月第一天
// 末尾
dayjs().endOf('year'); // 當年最后一天
2.4. 顯示
// 格式化
dayjs().format(); // 默認格式,如:2020-06-03T20:06:13+08:00
dayjs().format("YYYY-MM-DD HH:mm:ss"); // 指定格式 2020-06-03 20:07:12
// 獲取兩個 Day.js 對象的時間差,默認毫秒,可指定單位
dayjs('2020').diff(dayjs('1998')); // 694224000000
dayjs('2020').diff(dayjs('1998'), 'year'); // 22
// 時間戳
dayjs().valueOf(); // 毫秒
dayjs().unix(); // 秒
// 天數
dayjs('2020-07').daysInMonth(); // 31
// 原生 Date 對象
dayjs().toDate(); // Wed Jun 03 2020 20:13:40 GMT+0800 (China Standard Time)
// 返回 ISO 8601 格式的字符串
dayjs().toJSON(); // "2020-06-03T12:15:54.635Z"
dayjs().toISOString(); // "2020-06-03T12:16:48.199Z"
2.5. 查詢
*請認真填寫需求信息,我們會在24小時內與您取得聯系。