Skip to content

Commit

Permalink
perf: ⚡ 优化简易阅读模式性能
Browse files Browse the repository at this point in the history
  • Loading branch information
hymbz committed Aug 11, 2024
1 parent 739e008 commit 858f0fd
Show file tree
Hide file tree
Showing 7 changed files with 64 additions and 82 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"i18n-ally.pathMatcher": "{locale}.{ext}",
"i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true,
"typescript.preferences.importModuleSpecifier": "relative",
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.tsdk": "node_modules\\typescript\\lib"
}
6 changes: 4 additions & 2 deletions src/components/Manga/actions/imageLoad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ import { renderImgList, showImgList } from './renderPage';

/** 图片加载完毕的回调 */
export const handleImgLoaded = (i: number, e: HTMLImageElement) => {
// 内联图片元素被创建后立刻就会触发 load 事件,如果在调用这个函数前 url 发生改变
// 就会导致这里获得的是上个 url 图片的尺寸
if (!e.isConnected) return;
setState((state) => {
const img = state.imgList[i];
// 与图片全载一起使用时会出现 src 不一样的情况,需要跳过
if (!img || e.src !== encodeURI(img.src)) return;
if (img.width !== e.naturalWidth || img.height !== e.naturalHeight)
updateImgSize(state, i, e.naturalWidth, e.naturalHeight);
img.loadType = 'loaded';
Expand Down Expand Up @@ -123,6 +124,7 @@ export const checkImgSize = (index: number) => {
`#_${index} img`,
)!;
const timeoutId = setInterval(() => {
if (!imgDom?.isConnected) return clearInterval(timeoutId);
const img = store.imgList[index];
if (!img || img.loadType !== 'loading') return clearInterval(timeoutId);

Expand Down
5 changes: 4 additions & 1 deletion src/components/Manga/actions/operate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,10 @@ export const handleKeyDown = (e: KeyboardEvent) => {
case 'jump_to_home':
return _setState('activePageIndex', 0);
case 'jump_to_end':
return _setState('activePageIndex', store.pageList.length - 1);
return _setState(
'activePageIndex',
Math.max(0, store.pageList.length - 1),
);

case 'switch_page_fill':
return switchFillEffect();
Expand Down
5 changes: 4 additions & 1 deletion src/components/Manga/hooks/useInit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
scrollTo,
updatePageData,
updateShowRange,
placeholderSize,
} from '../actions';
import { defaultOption } from '../store/option';
import { playAnimation } from '../helper';
Expand All @@ -19,7 +20,7 @@ import { autoCloseFill } from '../handleComicData';
const createComicImg = (url: string): ComicImg => ({
src: url || '',
loadType: 'wait',
size: { width: 0, height: 0 },
size: placeholderSize(),
});

export const useInit = (props: MangaProps) => {
Expand Down Expand Up @@ -137,7 +138,9 @@ export const useInit = (props: MangaProps) => {
const imgMap = new Map(state.imgList.map((img) => [img.src, img]));
for (let i = 0; i < props.imgList.length; i++) {
const url = props.imgList[i];
// 只有旧图一张不剩才算是新漫画
if (isNew && imgMap.has(url)) isNew = false;
// 只要有加载好的旧图被删就要更新页面
const img = url && !needUpdatePageData && state.imgList[i];
if (img && img.loadType !== 'wait' && img.src && img.src !== url)
needUpdatePageData = true;
Expand Down
30 changes: 13 additions & 17 deletions src/helper/other.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ export async function wait<T>(
let res: T | undefined = await fn();
let _timeout = timeout;
while (_timeout > 0 && !res) {
await sleep(10);
await sleep(100);
_timeout -= 10;
res = await fn();
}
Expand All @@ -263,22 +263,18 @@ export const waitImgLoad = (
? window.setTimeout(() => reject(new Error('timeout')), timeout)
: undefined;

img.addEventListener(
'load',
() => {
window.clearTimeout(id);
resolve(img);
},
{ once: true },
);
img.addEventListener(
'error',
(e) => {
window.clearTimeout(id);
reject(new Error(e.message));
},
{ once: true },
);
const handleError = (e: ErrorEvent) => {
window.clearTimeout(id);
reject(new Error(e.message));
};
const handleLoad = () => {
window.clearTimeout(id);
img.removeEventListener('error', handleError);
resolve(img);
};

img.addEventListener('load', handleLoad, { once: true });
img.addEventListener('error', handleError, { once: true });

if (typeof target === 'string') img.src = target;
});
Expand Down
62 changes: 32 additions & 30 deletions src/site/other/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,7 @@ import { renderImgList, store } from 'components/Manga';
import { useInit, toast } from 'main';

import { getEleSelector, isEleSelector } from './eleSelector';
import {
needTrigged,
triggerLazyLoad,
openScrollLock,
} from './triggerLazyLoad';
import { needTrigged, triggerLazyLoad } from './triggerLazyLoad';

// 测试案例
// https://www.177picyy.com/html/2023/03/5505307.html
Expand Down Expand Up @@ -149,19 +145,30 @@ import {
return;
}

/** 找出应该是漫画图片,且还需要继续触发懒加载的图片个数 */
const expectCount = options.selector
? querySelectorAll<HTMLImageElement>(options.selector).filter(
needTrigged,
).length
: 0;
const _imgEleList = expectCount
? [...imgEleList, ...Array.from<null>({ length: expectCount })]
: imgEleList;
let newImgEleList: Array<HTMLImageElement | undefined> = imgEleList;

/** 预计的图片总数 */
let expectCount = 0;
/** 还需要继续触发懒加载的图片个数 */
let needTriggedNum = 0;
if (options.selector) {
const expectImgList = querySelectorAll<HTMLImageElement>(
options.selector,
);
expectCount = expectImgList.length;
needTriggedNum = expectImgList.filter(needTrigged).length;
// 根据预计的图片总数补上占位的空图
const fillImgNum = expectCount - imgEleList.length;
if (fillImgNum > 0)
newImgEleList = [
...imgEleList,
...Array.from<undefined>({ length: fillImgNum }),
];
}

let isEdited = false;
await plimit(
_imgEleList.map((e, i) => async () => {
newImgEleList.map((e, i) => async () => {
const newUrl = e ? await handleImgUrl(e) : '';
if (newUrl === mangaProps.imgList[i]) return;

Expand All @@ -172,12 +179,12 @@ import {
if (isEdited) saveImgEleSelector(imgEleList);

// colamanga 会创建随机个数的假 img 元素,导致刚开始时高估页数,需要再删掉多余的页数
if (mangaProps.imgList.length > _imgEleList.length)
setManga('imgList', mangaProps.imgList.slice(0, _imgEleList.length));
if (mangaProps.imgList.length > newImgEleList.length)
setManga('imgList', mangaProps.imgList.slice(0, newImgEleList.length));

if (
isEdited ||
expectCount ||
needTriggedNum ||
imgEleList.some((e) => !e.naturalWidth && !e.naturalHeight)
) {
if (updateImgListTimeout) window.clearTimeout(updateImgListTimeout);
Expand All @@ -188,11 +195,10 @@ import {
let timeout = false;

const triggerAllLazyLoad = () =>
triggerLazyLoad(getAllImg, () =>
// 只在`开启了阅读模式所以用户看不到网页滚动`和`当前可显示图片数量不足`时停留一段时间
mangaProps.show || (!timeout && mangaProps.imgList.length === 0)
? 300
: 0,
triggerLazyLoad(
getAllImg,
// 只在`开启了阅读模式`和`当前可显示图片数量不足`时通过滚动触发懒加载
() => mangaProps.show || (!timeout && mangaProps.imgList.length === 0),
);

/** 监视页面元素发生变化的 Observer */
Expand Down Expand Up @@ -243,7 +249,6 @@ import {
behavior: 'instant',
block: 'end',
});
openScrollLock(500);
}, 1000),
{ defer: true },
);
Expand All @@ -254,17 +259,14 @@ import {
() => store.show,
(show) => {
if (show) laseScroll = window.scrollY;
else {
openScrollLock(1000);
// 稍微延迟一下,等之前触发懒加载时的滚动结束
requestAnimationFrame(() => window.scrollTo(0, laseScroll));
}
// 稍微延迟一下,等之前触发懒加载时的滚动结束
else requestAnimationFrame(() => window.scrollTo(0, laseScroll));
},
);
};

if ((await GM.getValue(window.location.hostname)) !== undefined)
return start();
return requestIdleCallback(start);

const menuId = await GM.registerMenuCommand(
extractI18n('site.simple.simple_read_mode')(await getInitLang()),
Expand Down
36 changes: 6 additions & 30 deletions src/site/other/triggerLazyLoad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,6 @@ const createImgData = (oldSrc = ''): ImgData => ({
oldSrc,
});

// 使用 triggerEleLazyLoad 会导致正常的滚动在滚到一半时被打断,所以加个锁限制一下
const scrollLock = {
enabled: false,
nextOpenTime: 0,
timeout: 0,
};
const closeScrollLock = (delay: number) => {
const time = Date.now() + delay;
if (time <= scrollLock.nextOpenTime) return;
scrollLock.nextOpenTime = time;
window.clearInterval(scrollLock.timeout);
scrollLock.timeout = window.setTimeout(() => {
scrollLock.enabled = false;
scrollLock.timeout = 0;
}, delay);
};

export const openScrollLock = (time: number) => {
scrollLock.enabled = true;
closeScrollLock(time);
};

window.addEventListener('wheel', () => openScrollLock(1000));

/** 用于判断是否是图片 url 的正则 */
const isImgUrlRe =
/^(((https?|ftp|file):)?\/)?\/[-\w+&@#/%?=~|!:,.;]+[-\w+&@#%=~|]$/;
Expand Down Expand Up @@ -152,12 +128,14 @@ const triggerTurnPage = async (waitTime = 0) => {
window.scroll({ top: nowScroll, behavior: 'instant' });
};

const waitTime = 300;

/** 触发页面上所有图片元素的懒加载 */
export const triggerLazyLoad = singleThreaded(
async (
state,
getAllImg: () => HTMLImageElement[],
getWaitTime: () => number,
runCondition: () => boolean,
) => {
// 过滤掉已经被触发过懒加载的图片
const targetImgList = getAllImg()
Expand All @@ -172,19 +150,17 @@ export const triggerLazyLoad = singleThreaded(
}

for (const e of targetImgList) {
await wait(() => !scrollLock.enabled);
const waitTime = getWaitTime();
await wait(runCondition);

await triggerTurnPage(waitTime);

if (!needTrigged(e)) continue;
tryCorrectUrl(e);

if (
(await triggerEleLazyLoad(e, waitTime, () =>
await triggerEleLazyLoad(e, waitTime, () =>
isLazyLoaded(e, imgMap.get(e)?.oldSrc),
)) ||
waitTime
)
)
handleTrigged(e);
}
Expand Down

0 comments on commit 858f0fd

Please sign in to comment.