JSの実行時間が長すぎてページがフリーズする問題の解決方法

バックグラウンド

ご存じのように、JavaScript (JS と呼ばれます) はシングル スレッド言語であり、ブラウザーは JS に 1 つのメイン スレッドのみを割り当て、タスク キューが空になるまで毎回タスク キューからタスクを実行します。これにより一部のブラウザがフリーズすることは避けられません. たとえば、今回のバックエンド フィードバックでは、ファイル アップロード タスクでは、JS はファイルを分割してサーバーにアップロードする必要がありますが、大きなファイル (数百ギガバイト) に遭遇すると、. ) 場合によっては、何十万ものコピーを断片化する必要があり、これらの断片が徐々にサーバーにアップロードされます。このプロセスで JS がこれらのファイル フラグメントを処理するのに少し時間がかかり、結果としてページ レンダリング fps が低くなり、ページがフリーズするためだと思います。そこで、JS の実行時間が長いためにページがフリーズする問題を解決するにはどうすればよいか考え始めました。

ソリューション

JS に関する私の個人的な理解によると、この種の行き詰まった問題に直面した場合の解決策は次のとおりです。

  • Web Workerを使用して新しいスレッドを開き、新しいスレッドを介して複雑で長いタスクを処理し、メイン スレッドがブロックされることによるページのフリーズを回避します。
  • ジェネレーター関数を使用して、長いタスクを複数のマクロ タスクに分割し、それらすべてがマイクロ タスク キューに押し込まれてページ レンダリングに影響を与えないようにします。
  • requestAnimationFrame

長い仕事

JS が一定時間内に継続的に実行する必要があるタスク。
次のコードは、長いプロセスを実行する JS の状況をシミュレートします。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<style>
    @keyframes move {
      
      
        from {
      
      
            left: 0;
        }

        to {
      
      
            left: 100%;
        }
    }

    .move {
      
      
        position: absolute;
        animation: move 5s linear infinite;
    }
</style>

<body>
    <div class="move">LHX-FLY</div>
</body>
<script>
    function longFun() {
      
      
        let i = 0
        const start = performance.now()
        // 5秒内不停执行 i++ 操作,实现长任务效果
        while (performance.now() - start <= 5000) {
      
      
            i++
        }
        return i
    }
    // 1秒后才执行js长任务
    setTimeout(() => {
      
      
        longFun()
    }, 1000)

</script>

効果は次のとおりです。
ここに画像の説明を挿入
ページを更新した後、アニメーション効果は 1 秒前はまだ滑らかですが、1 秒後は、JS が長いタスクを処理する必要があるため、ページをレンダリングできず、アニメーションが立ち往生しています。

ここに画像の説明を挿入
合計実行時間が 5 秒を超えていることも、コンソールのパフォーマンスからわかります。1 秒以降は、CPU メイン スレッドが基本的に占有されています。つまり、この期間中、インターフェースはアニメーションのレンダリングを停止します。

ウェブワーカー

上記の長いタスク ラグの問題に対して、Web Worker の解決コードは次のとおりです。

// 创建线程函数
function createWorker(f) {
    
    
    var blob = new Blob(['(' + f.toString() + ')()']);
    var url = window.URL.createObjectURL(blob);
    var worker = new Worker(url);
    return worker;
}
 // 1秒后才执行js长任务
 setTimeout(() => {
    
    
     createWorker(longFun); // 用新的线程去执行这个长任务
     // longFun()
}, 1000)   

実行結果は次のとおりです。ここに画像の説明を挿入
ここに画像の説明を挿入
ページを更新した後も、インターフェイス全体のアニメーション効果は非常にスムーズであることがわかります。合計実行時間は変わらず 5 秒を超えていますが、メインスレッドの実行時間はわずか 54 ミリ秒であり、新しいスレッドの実行パフォーマンスは Idle に起因しており、CPU は常に占有されているわけではありません。

ジェネレーター関数

ジェネレーター関数を使用して問題を解決する前に、まず、下の図に示すように、JS の実行プロセス、つまりイベント サイクルの一般的なプロセスを理解する必要があります
ここに画像の説明を挿入
。特別な場合を除いて、ページのレンダリングはマイクロタスク キューに入れられます。クリア後、マクロ タスクを実行する前に。そのため、メイン実行スタックにプッシュされた関数を一定期間実行してスリープ状態にし、レンダリング後にマクロ タスクで関数をウェイクアップして、レンダリングやユーザー インタラクションが停止しないようにすることができます。
Microtask
Promise
process.nextTick
Object.observe
MutaionObserver
...コードスクリプト全体を含む
マクロタスクsetTimeout setIntervcal I/O postMessage MessageChannel ...






実際、ジェネレーター関数は長いタスクをステップに分割するだけであり、各ステップの間に一時停止があります (1 つのステップはマイクロタスクであり、ジェネレーター関数が次に実行されない場合、次のステップは実行されず、マイクロタスクキューはこの時点でクリアされ、次のタスクは次のステップまで実行されません)。上訴問題をジェネレータ関数に変換すると、次の形式になります。

// 将原来的长任务改成 generator 函数
function* fnc_() {
    
    
    let i = 0
    const start = performance.now()
    while (performance.now() - start <= 5000) {
    
    
        yield i++
    }
    return i
}

// 简易时间分片 
function timeSlice (fnc) {
    
     
    if(fnc.constructor.name !== 'GeneratorFunction') return fnc() 
    
    return async function (...args) {
    
     
        const fnc_ = fnc(...args) 
        let data 
        do {
    
     
            data = fnc_.next()
        } while (!data.done) 
        return data.value 
    } 
}
 // 1秒后才执行js长任务
 setTimeout(() => {
    
    
     const fnc = timeSlice(fnc_)
     const start = performance.now()
     console.log('开始')
     const num = await fnc()
     console.log('结束', `${
      
      (performance.now() - start) / 1000}s`)
     console.log(num)
}, 1000)  

次のパフォーマンス結果から、アピール コードが元の Caton とまったく同じであることがわかります. アピール コードは、長いタスクをジェネレータ イテレータに変換するだけであり、fnc_ 関数全体がマクロ タスクに関連しており、その後timeSlice関数を使用して、次のステップを実行するためにNon-stop nextにする(各ステップはマイクロタスクですが、次のステップがすぐに実行されない場合は、現在のステップのマイクロタスクキューが実行され、次の命令を待ちます) done が完了するまでですが、合計時間は よりも長くなります。実行に時間がかかることがわかります。ここに画像の説明を挿入
ここに画像の説明を挿入
ただし、次の行に次のコードを追加します。

do {
    
    
    data = fnc_.next()
    // 每执行一步就休眠,注册一个宏任务 setTimeout 来叫醒他
    await new Promise(resolve => setTimeout(resolve))
} while (!data.done)

実際には、ジェネレーターの次は、ウェイクアップ後に実行される操作です. await new Promise(resolve => setTimeout(resolve)) はsetTimeout マクロ タスクを生成します。 sleep この間、microtask キューはクリアされたと見なすことができ、他のタスクが最初に実行され、ページもレンダリングされます. イベントループを実行した後、次に実行させて、連続実行時間が長くならないようにします.とても長いですが、実行されます ステップごとに実行に分割され、タスクキューの実行が完了するまでループで実行され続けます。
ここに画像の説明を挿入
アニメーション効果は常に滑らかであることがわかります
ここに画像の説明を挿入
が、上記のタイム スライスは実際には各ステップでスリープ状態になるため、実行効率は比較的低く、タイム スライス機能を最適化できます。

// 精准时间分片 
function timeSlice_(fnc, time = 25) {
    
    
    if (fnc.constructor.name !== 'GeneratorFunction') return fnc()
    return function (...args) {
    
    
        const fnc_ = fnc(...args)
        function go() {
    
    
            const start = performance.now()
            let data
            do {
    
    
                data = fnc_.next()
            } while (!data.done && performance.now() - start < time)
            if (data.done) return data.value
            return new Promise((resolve, reject) => {
    
    
                setTimeout(() => {
    
    
                    try {
    
     resolve(go()) } catch (e) {
    
     reject(e) }
                })
            })
        }
        return go()
    }
}    

最適化後のパフォーマンス結果を下の図に示します (アニメーション効果は滑らかです): 最適化
ここに画像の説明を挿入
前と最適化後のタイム スライス関数のパフォーマンスを比較すると、CPU の実行が最適化前のセクションに分割されていることがわかります。ですが、最適化後はセクションに分かれています。
しかし、それらの 2 つの印刷結果を比較してみましょう:
最適化前:
ここに画像の説明を挿入
最適化後:
ここに画像の説明を挿入
このことから、i の値が大きく異なることがわかります。最適化前の 1037 と最適化後の 6042162 は、どちらも 5 秒で実行されます。期間内の JS 実行数が反映されている.最適化前は 1037 回しか実行されていなかったが,最適化後は 6,042,162 回の実行が可能である.同時実行数を効率とみなすことができ,最適化後の効率
ここに画像の説明を挿入
は最適化前の 5826 倍です。

ジェネレーター関数がタイム スライスによってページ レンダリングを最適化する方法を
理解した後、長いタスクのジェネレーターの変換に注意を払う必要があることに注意してください (これは、コードを直接実行する Web ワーカーとは異なり、個々のユーザーがどのように変更するかによって異なります)。元のコード) , yield の位置は非常に重要であり, 時間のかかる実行場所に配置する必要があります. 例えば, 上記の例の長いタスクは 5 秒以内のノンストップ i++ です.

requestAnimationFrame

このメソッドは主に JS アニメーションのフリーズを解決するために使用されるため、ブラウザの CSS アニメーションをブロックする上記の長いタスクは適用できません。

序章

requestAnimationFrame は、 rAFと呼ばれる HTML5 で提供されるアニメーション API です. Web アプリケーションでは、アニメーション効果を実現する方法がたくさんあります. Javascriptでは、タイマー setTimeout または setInterval によって実現できます. css3では、トランジションとアニメーションを使用できます. html5の Canvas も実現できます。さらに、html5 は、アニメーションの要求専用の API、つまりrequestAnimationFrameも提供します。これは、その名前が示すように、アニメーション フレームを要求します

関連概念

画面のリフレッシュ レート画面
のリフレッシュ レートは、コンピューターの「ディスプレイの詳細設定」で確認できますが、一般的には 60Hz です 「ビジュアル ステイ効果」は、変化やジッターを感じず、表示されるものは依然として連続しています。実際、中間間隔は 16.7ms (つまり 1000/60) です。

ページが表示されて
いる ページが最小化されるか、バックグラウンド タブ ページに切り替えられると、ページは非表示になり、ブラウザーは visibilitychange イベントをトリガーし、document.hidden プロパティを true に設定します。表示状態に切り替えると、ページが表示され、document.hidden プロパティを false に設定して、visibilitychange イベントをトリガーします。

アニメーション フレーム リクエスト コールバック関数リスト
各 Document には、アニメーション フレーム リクエスト コールバック関数リストがあり、<handlerId, callback> タプルのセットと見なすことができます。このうち、handlerId はリスト内のタプルの位置を一意に識別する整数で、callback はコールバック関数です。

requestAnimationFrame を使用する理由

ただし、一部のアニメーション効果が JS を介して実現されている場合、たとえば setTimeout を使用すると、setTimeout は実際には時間間隔で画像を継続的に更新することによって形成されるアニメーション効果です。ただし、一部のモデルや複雑なアプリケーションでは setTimeout がフリーズする場合があり、これは「フレーム ロス」と呼ばれることがよくあります。これは、setTimeout は固定の時間間隔しか設定できず、画面やモデルが異なれば解像度も異なり、setTimeout タスクは非同期キューに入れられるため、実際の実行時間は設定された時間よりも遅くなります。これらの理由により、setTimeout アニメーションの停止現象が発生します。
setTimeout の欠点を知っていれば、rAF の出現は当然のことです。rAF コールバック関数の実行タイミングはシステムによって決定されます. つまり, システムは各描画の前に rAF でコールバック関数を積極的に呼び出します. システムの描画周波数が 60Hz の場合, コールバック関数は 16.7 に 1 回実行されます. ms. システムの描画周波数が 75Hz の場合、時間間隔は 1000/75=13.3ms であり、各描画の途中でコールバック関数を 1 回実行できるため、フレームの損失や途切れが発生しません。
さらに、requestAnimationFrame には次の 2 つの利点があります。

  • CPU の省電力: setTimeout を使用してアニメーションを実装すると、ページが非表示または最小化されている場合でも、setTimeout はバックグラウンドでアニメーション タスクを実行します. この時点でページは非表示または使用できないため、アニメーションを更新しても意味がありません。 CPU リソースの浪費です。requestAnimationFrame は全く別物です.ページ処理が活性化されていない場合,ページの画面更新タスクもシステムによって中断されるため,システムに続く requestAnimationFrame もレンダリングを停止します.ページが活性化されると,アニメーションは前回から開始. そのままの場所で実行を継続し、CPU オーバーヘッドを効果的に節約します.
  • 関数のスロットリング: 高頻度のイベント (サイズ変更、スクロールなど) では、更新間隔内で複数の関数が実行されるのを防ぐために、requestAnimationFrame を使用して、各更新間隔内で関数が 1 回だけ実行されるようにします。これにより、滑らかなパフォーマンスを確保できます。 、および関数実行のオーバーヘッドをより適切に節約できます。16.7ms ごとに表示が更新され、複数回の描画が画面に反映されないため、更新間隔内で関数を複数回実行しても意味がありません。

使用

インターネット上で最も一般的な単純な例 (rAF は必ずしもアニメーションに使用されるとは限りません。頻繁に実行される JS コードを解決するのにも適しています):

var progress = 0;
//回调函数
function render() {
    
    
  progress += 1; //修改图像的位置
  if (progress < 100) {
    
    
    //在动画没有结束前,递归渲染
    window.requestAnimationFrame(render);
  }
}
//第一帧渲染
window.requestAnimationFrame(render);

このメソッドは非同期です。渡された関数は、アニメーションが再描画される前に呼び出されます。

// 传入一个callback函数,即动画函数;
// 返回值handlerId为浏览器定义的、大于0的整数,唯一标识了该回调函数在列表中位置。
handlerId = requestAnimationFrame(callback)

実行プロセス:
(1) まず、document.hidden プロパティが true であるかどうか、つまり、ページが表示されている場合にのみ実行されるかどうかを判断する必要があります; ( 2) ブラウザーはアニメーション関数の前のラウンドをクリアします
。 ;
(3) このメソッドによって返される handlerId 値は、アニメーション関数のコールバックになります。<handlerId , callback> を使用して、アニメーション フレーム要求のコールバック関数リストに入ります; (4) ブラウザは、アニメーション フレーム要求のコールバック関数リストをトラバースします
。 handlerId の値に従って、対応するアニメーション関数を順番に実行します。

具体例

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<style>
    .move {
    
    
        position: absolute;
    }
</style>

<body>
    <div class="move" id="box">LHX-FLY</div>
</body>
<script>
    var box = document.getElementById('box')
    var flag = false
    var left = 0
    function render() {
    
    
        if (flag) {
    
    
            if (left >= 100) {
    
    
                flag = false
            }
            box.style.left = `${
      
      left++}px`
        } else {
    
    
            if (left <= 0) {
    
    
                flag = true
            }
            box.style.left = `${
      
      left--}px`
        }
        window.requestAnimationFrame(render)
    }
    render()
</ script>    

コード実行の効果は、次の図に示すとおりです。
ここに画像の説明を挿入

実際、アピールのサンプルコードから、renderでrenderを呼び出すのは非常に危険です.メソッドは再帰的ですが、判定は追加されていません.通常、ブラウザはエラーを報告しますが、ウィンドウで実行されません. .requestAnimationFrame コールバック (ただし、このように記述することはお勧めしません)
requestAnimationFrame でコールバックをキャンセルして実行し続けるにはどうすればよいですか?
cancelAnimationFrame メソッドを呼び出すだけです。

 var rAFId = null;
 function render() {
    
    
    if (flag) {
    
    
        if (left >= 100) {
    
    
            flag = false
        }
        box.style.left = `${
      
      left++}px`
    } else {
    
    
        if (left <= 0) {
    
    
            flag = true
        }
        box.style.left = `${
      
      left--}px`
    }
    rAFId =  window.requestAnimationFrame(render)
    // 5 秒后取消动画效果
    setTimeout(function () {
    
    
        cancelAnimationFrame(rAFId)
    }, 5000)
}

効果は図に示すとおりです。5
ここに画像の説明を挿入
秒後に、アニメーション効果が実際にキャンセルされていることがわかります。

グレースフル ダウングレード

requestAnimationFrame にはまだ互換性の問題があり、ブラウザーごとに異なるプレフィックスを使用する必要があるためです。そのため、graceful degradation で requestAnimationFrame をカプセル化し、高度な機能の使用を優先し、setTimeout のみが使用できるようになるまで、さまざまなブラウザーの状況に応じてロールバックする必要があります。

;(function () {
    
    
	var lastTime = 0
	var vendors = ['ms', 'moz', 'webkit', 'o']
	for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
    
    
		window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']
		window.cancelAnimationFrame =
			window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame']
	}

	if (!window.requestAnimationFrame)
		window.requestAnimationFrame = function (callback, element) {
    
    
			var currTime = new Date().getTime()
			var timeToCall = Math.max(0, 16 - (currTime - lastTime))
			var id = window.setTimeout(function () {
    
    
				callback(currTime + timeToCall)
			}, timeToCall)
			lastTime = currTime + timeToCall
			return id
		}

	if (!window.cancelAnimationFrame)
		window.cancelAnimationFrame = function (id) {
    
    
			clearTimeout(id)
		}
})()

参考記事:
タイムスライシング技術(長いjsタスクによるページフリーズを解決する)
requestAnimationFrameの使い方

おすすめ

転載: blog.csdn.net/weixin_43589827/article/details/122496049