摘要:?jiǎn)?dòng)性能瓶頸分析與解決方案翻譯自的,從屬于筆者的前端入門與工程實(shí)踐。我們必須要清醒地認(rèn)識(shí)到全面評(píng)測(cè)以挖掘出真正性能瓶頸的重要性。這可能是最佳的方式了,類似于這樣的模式鼓勵(lì)基于路由的分組,目前被與廣泛使用。
JavaScript 啟動(dòng)性能瓶頸分析與解決方案 翻譯自 Addy Osmani 的 JavaScript Start-up Performance,從屬于筆者的Web 前端入門與工程實(shí)踐。本文已獲得原作者授權(quán),為InfoQ中文站特供稿件,首發(fā)地址為這里;如需轉(zhuǎn)載,請(qǐng)與InfoQ中文站聯(lián)系。隨著現(xiàn)代 Web 技術(shù)的發(fā)展與用戶交互復(fù)雜度的增加,我們的網(wǎng)站變得日益臃腫,也要求著我們不斷地優(yōu)化網(wǎng)站性能以保證友好的用戶體驗(yàn)。本文作者則著眼于 JavaScript 啟動(dòng)階段優(yōu)化,首先以大量的數(shù)據(jù)分析闡述了語(yǔ)法分析、編譯等步驟耗時(shí)占比過(guò)多是很多網(wǎng)站的性能瓶頸之一。然后作者提供了一系列用于在現(xiàn)代瀏覽器中進(jìn)行性能評(píng)測(cè)的工具,還分別從開發(fā)者工程實(shí)踐與 JavaScript 引擎內(nèi)部實(shí)現(xiàn)的角度闡述了應(yīng)當(dāng)如何提高解析與編譯速度。
在 Web 開發(fā)中,隨著需求的增加與代碼庫(kù)的擴(kuò)張,我們最終發(fā)布的 Web 頁(yè)面也逐漸膨脹。不過(guò)這種膨脹遠(yuǎn)不止意味著占據(jù)更多的傳輸帶寬,其還意味著用戶瀏覽網(wǎng)頁(yè)時(shí)可能更差勁的性能體驗(yàn)。瀏覽器在下載完某個(gè)頁(yè)面依賴的腳本之后,其還需要經(jīng)過(guò)語(yǔ)法分析、解釋與運(yùn)行這些步驟。而本文則會(huì)深入分析瀏覽器對(duì)于 JavaScript 的這些處理流程,挖掘出那些影響你應(yīng)用啟動(dòng)時(shí)間的罪魁禍?zhǔn)?,并且根?jù)我個(gè)人的經(jīng)驗(yàn)提出相對(duì)應(yīng)的解決方案?;仡欉^(guò)去,我們還沒(méi)有專門地考慮過(guò)如何去優(yōu)化 JavaScript 解析/編譯這些步驟;我們預(yù)想中的是解析器在發(fā)現(xiàn)標(biāo)簽后會(huì)瞬時(shí)完成解析操作,不過(guò)這很明顯是癡人說(shuō)夢(mèng)。下圖是對(duì)于 V8 引擎工作原理的概述:
下面我們深入其中的關(guān)鍵步驟進(jìn)行分析。
在啟動(dòng)階段,語(yǔ)法分析,編譯與腳本執(zhí)行占據(jù)了 JavaScript 引擎運(yùn)行的絕大部分時(shí)間。換言之,這些過(guò)程造成的延遲會(huì)真實(shí)地反應(yīng)到用戶可交互時(shí)延上;譬如用戶已經(jīng)看到了某個(gè)按鈕,但是要好幾秒之后才能真正地去點(diǎn)擊操作,這一點(diǎn)會(huì)大大影響用戶體驗(yàn)。
上圖是我們使用 Chrome Canary 內(nèi)置的 V8 RunTime Call Stats 對(duì)于某個(gè)網(wǎng)站的分析結(jié)果;需要注意的是桌面瀏覽器中語(yǔ)法解析與編譯占用的時(shí)間還是蠻長(zhǎng)的,而在移動(dòng)端中占用的時(shí)間則更長(zhǎng)。實(shí)際上,對(duì)于 Facebook, Wikipedia, Reddit 這些大型網(wǎng)站中語(yǔ)法解析與編譯所占的時(shí)間也不容忽視:
上圖中的粉色區(qū)域表示花費(fèi)在 V8 與 Blink"s C++ 中的時(shí)間,而橙色和黃色分別表示語(yǔ)法解析與編譯的時(shí)間占比。Facebook 的 Sebastian Markbage 與 Google 的 Rob Wormald 也都在 Twitter 發(fā)文表示過(guò) JavaScript 的語(yǔ)法解析時(shí)間過(guò)長(zhǎng)已經(jīng)成為了不可忽視的問(wèn)題,后者還表示這也是 Angular 啟動(dòng)時(shí)主要的消耗之一。
隨著移動(dòng)端浪潮的涌來(lái),我們不得不面對(duì)一個(gè)殘酷的事實(shí):移動(dòng)端對(duì)于相同包體的解析與編譯過(guò)程要花費(fèi)相當(dāng)于桌面瀏覽器2~5倍的時(shí)間。當(dāng)然,對(duì)于高配的 iPhone 或者 Pixel 這樣的手機(jī)相較于 Moto G4 這樣的中配手機(jī)表現(xiàn)會(huì)好很多;這一點(diǎn)提醒我們?cè)跍y(cè)試的時(shí)候不能僅用身邊那些高配的手機(jī),而應(yīng)該中高低配兼顧:
上圖是部分桌面瀏覽器與移動(dòng)端瀏覽器對(duì)于 1MB 的 JavaScript 包體進(jìn)行解析的時(shí)間對(duì)比,顯而易見的可以發(fā)現(xiàn)不同配置的移動(dòng)端手機(jī)之間的巨大差異。當(dāng)我們應(yīng)用包體已經(jīng)非常巨大的時(shí)候,使用一些現(xiàn)代的打包技巧,譬如代碼分割,TreeShaking,Service Workder 緩存等等會(huì)對(duì)啟動(dòng)時(shí)間有很大的影響。另一個(gè)角度來(lái)看,即使是小模塊,你代碼寫的很糟或者使用了很糟的依賴庫(kù)都會(huì)導(dǎo)致你的主線程花費(fèi)大量的時(shí)間在編譯或者冗余的函數(shù)調(diào)用中。我們必須要清醒地認(rèn)識(shí)到全面評(píng)測(cè)以挖掘出真正性能瓶頸的重要性。
JavaScript 語(yǔ)法解析與編譯是否成為了大部分網(wǎng)站的瓶頸?我曾不止一次聽到有人說(shuō),我又不是 Facebook,你說(shuō)的 JavaScript 語(yǔ)法解析與編譯到
底會(huì)對(duì)其他網(wǎng)站造成什么樣的影響呢?對(duì)于這個(gè)問(wèn)題我也很好奇,于是我花費(fèi)了兩個(gè)月的時(shí)間對(duì)于超過(guò) 6000 個(gè)網(wǎng)站進(jìn)行分析;這些網(wǎng)站囊括了 React,Angular,Ember,Vue 這些流行的框架或者庫(kù)。大部分的測(cè)試是基于 WebPageTest 進(jìn)行的,因此你可以很方便地重現(xiàn)這些測(cè)試結(jié)果。光纖接入的桌面瀏覽器大概需要 8 秒的時(shí)間才能允許用戶交互,而 3G 環(huán)境下的 Moto G4 大概需要 16 秒 才能允許用戶交互。
大部分應(yīng)用在桌面瀏覽器中會(huì)耗費(fèi)約 4 秒的時(shí)間進(jìn)行 JavaScript 啟動(dòng)階段(語(yǔ)法解析、編譯、執(zhí)行):
而在移動(dòng)端瀏覽器中,大概要花費(fèi)額外 36% 的時(shí)間來(lái)進(jìn)行語(yǔ)法解析:
另外,統(tǒng)計(jì)顯示并不是所有的網(wǎng)站都甩給用戶一個(gè)龐大的 JS 包體,用戶下載的經(jīng)過(guò) Gzip 壓縮的平均包體大小是 410KB,這一點(diǎn)與 HTTPArchive 之前發(fā)布的 420KB 的數(shù)據(jù)基本一致。不過(guò)最差勁的網(wǎng)站則是直接甩了 10MB 的腳本給用戶,簡(jiǎn)直可怕。
通過(guò)上面的統(tǒng)計(jì)我們可以發(fā)現(xiàn),包體體積固然重要,但是其并非唯一因素,語(yǔ)法解析與編譯的耗時(shí)也不一定隨著包體體積的增長(zhǎng)而線性增長(zhǎng)。總體而言小的 JavaScript 包體是會(huì)加載地更快(忽略瀏覽器、設(shè)備與網(wǎng)絡(luò)連接的差異),但是同樣 200KB 的大小,不同開發(fā)者的包體在語(yǔ)法解析、編譯上的時(shí)間卻是天差地別,不可同日而語(yǔ)。
現(xiàn)代 JavaScript 語(yǔ)法解析 & 編譯性能評(píng)測(cè) Chrome DevTools打開 Timeline( Performance panel ) > Bottom-Up/Call Tree/Event Log 就會(huì)顯示出當(dāng)前網(wǎng)站在語(yǔ)法解析/編譯上的時(shí)間占比。如果你希望得到更完整的信息,那么可以打開 V8 的 Runtime Call Stats。在 Canary 中,其位于 Timeline 的 Experims > V8 Runtime Call Stats 下。
打開 about:tracing 頁(yè)面,Chrome 提供的底層的追蹤工具允許我們使用disabled-by-default-v8.runtime_stats來(lái)深度了解 V8 的時(shí)間消耗情況。V8 也提供了詳細(xì)的指南來(lái)介紹如何使用這個(gè)功能。
WebPageTest 中 Processing Breakdown 頁(yè)面在我們啟用 Chrome > Capture Dev Tools Timeline 時(shí)會(huì)自動(dòng)記錄 V8 編譯、EvaluateScript 以及 FunctionCall 的時(shí)間。我們同樣可以通過(guò)指明disabled-by-default-v8.runtime_stats的方式來(lái)啟用 Runtime Call Stats。
更多使用說(shuō)明參考我的gist。
User Timing我們還可以使用 Nolan Lawson 推薦的User Timing API來(lái)評(píng)估語(yǔ)法解析的時(shí)間。不過(guò)這種方式可能會(huì)受 V8 預(yù)解析過(guò)程的影響,我們可以借鑒 Nolan 在 optimize-js 評(píng)測(cè)中的方式,在腳本的尾部添加隨機(jī)字符串來(lái)解決這個(gè)問(wèn)題。我基于 Google Analytics 使用相似的方式來(lái)評(píng)估真實(shí)用戶與設(shè)備訪問(wèn)網(wǎng)站時(shí)候的解析時(shí)間:
Etsy 的 DeviceTiming 工具能夠模擬某些受限環(huán)境來(lái)評(píng)估頁(yè)面的語(yǔ)法解析與執(zhí)行時(shí)間。其將本地腳本包裹在了某個(gè)儀表工具代碼內(nèi)從而使我們的頁(yè)面能夠模擬從不同的設(shè)備中訪問(wèn)??梢蚤喿x Daniel Espeset 的Benchmarking JS Parsing and Execution on Mobile Devices 一文來(lái)了解更詳細(xì)的使用方式。
減少 JavaScript 包體體積。我們?cè)谏衔闹幸蔡峒埃〉陌w往往意味著更少的解析工作量,也就能降低瀏覽器在解析與編譯階段的時(shí)間消耗。
使用代碼分割工具來(lái)按需傳遞代碼與懶加載剩余模塊。這可能是最佳的方式了,類似于PRPL這樣的模式鼓勵(lì)基于路由的分組,目前被 Flipkart, Housing.com 與 Twitter 廣泛使用。
Script streaming: 過(guò)去 V8 鼓勵(lì)開發(fā)者使用async/defer來(lái)基于script streaming實(shí)現(xiàn) 10-20% 的性能提升。這個(gè)技術(shù)會(huì)允許 HTML 解析器將相應(yīng)的腳本加載任務(wù)分配給專門的 script streaming 線程,從而避免阻塞文檔解析。V8 推薦盡早加載較大的模塊,畢竟我們只有一個(gè) streamer 線程。
評(píng)估我們依賴的解析消耗。我們應(yīng)該盡可能地選擇具有相同功能但是加載地更快的依賴,譬如使用 Preact 或者 Inferno 來(lái)代替 React,二者相較于 React 體積更小具有更少的語(yǔ)法解析與編譯時(shí)間。Paul Lewis 在最近的一篇文章中也討論了框架啟動(dòng)的代價(jià),與 Sebastian Markbage 的說(shuō)法不謀而合:最好地評(píng)測(cè)某個(gè)框架啟動(dòng)消耗的方式就是先渲染一個(gè)界面,然后刪除,最后進(jìn)行重新渲染。第一次渲染的過(guò)程會(huì)包含了分析與編譯,通過(guò)對(duì)比就能發(fā)現(xiàn)該框架的啟動(dòng)消耗。
如果你的 JavaScript 框架支持 AOT(ahead-of-time)編譯模式,那么能夠有效地減少解析與編譯的時(shí)間。Angular 應(yīng)用就受益于這種模式:
不用灰心,你并不是唯一糾結(jié)于如何提升啟動(dòng)時(shí)間的人,我們 V8 團(tuán)隊(duì)也一直在努力。我們發(fā)現(xiàn)之前的某個(gè)評(píng)測(cè)工具 Octane 是個(gè)不錯(cuò)的對(duì)于真實(shí)場(chǎng)景的模擬,它在微型框架與冷啟動(dòng)方面很符合真實(shí)的用戶習(xí)慣。而基于這些工具,V8 團(tuán)隊(duì)在過(guò)去的工作中也實(shí)現(xiàn)了大約 25% 的啟動(dòng)性能提升:
本部分我們就會(huì)對(duì)過(guò)去幾年中我們使用的提升語(yǔ)法解析與編譯時(shí)間的技巧進(jìn)行闡述。
代碼緩存Chrome 42 開始引入了所謂的代碼緩存的概念,為我們提供了一種存放編譯后的代碼副本的機(jī)制,從而當(dāng)用戶二次訪問(wèn)該頁(yè)面時(shí)可以避免腳本抓取、解析與編譯這些步驟。除以之外,我們還發(fā)現(xiàn)在重復(fù)訪問(wèn)的時(shí)候這種機(jī)制還能避免 40% 左右的編譯時(shí)間,這里我會(huì)深入介紹一些內(nèi)容:
代碼緩存會(huì)對(duì)于那些在 72 小時(shí)之內(nèi)重復(fù)執(zhí)行的腳本起作用。
對(duì)于 Service Worker 中的腳本,代碼緩存同樣對(duì) 72 小時(shí)之內(nèi)的腳本起作用。
對(duì)于利用 Service Worker 緩存在 Cache Storage 中的腳本,代碼緩存能在腳本首次執(zhí)行的時(shí)候起作用。
總而言之,對(duì)于主動(dòng)緩存的 JavaScript 代碼,最多在第三次調(diào)用的時(shí)候其能夠跳過(guò)語(yǔ)法分析與編譯的步驟。我們可以通過(guò)chrome://flags/#v8-cache-strategies-for-cache-storage來(lái)查看其中的差異,也可以設(shè)置?js-flags=profile-deserialization運(yùn)行 Chrome 來(lái)查看代碼是否加載自代碼緩存。不過(guò)需要注意的是,代碼緩存機(jī)制僅會(huì)緩存那些經(jīng)過(guò)編譯的代碼,主要是指那些頂層的往往用于設(shè)置全局變量的代碼。而對(duì)于類似于函數(shù)定義這樣懶編譯的代碼并不會(huì)被緩存,不過(guò) IIFE 同樣被包含在了 V8 中,因此這些函數(shù)也是可以被緩存的。
Script StreamingScript Streaming允許在后臺(tái)線程中對(duì)異步腳本執(zhí)行解析操作,可以對(duì)于頁(yè)面加載時(shí)間有大概 10% 的提升。上文也提到過(guò),這個(gè)機(jī)制同樣會(huì)對(duì)同步腳本起作用。
這個(gè)特性倒是第一次提及,因此 V8 會(huì)允許所有的腳本,即使阻塞型的腳本也可以由后臺(tái)線程進(jìn)行解析。不過(guò)缺陷就是目前僅有一個(gè) streaming 后臺(tái)線程存在,因此我們建議首先解析大的、關(guān)鍵性的腳本。在實(shí)踐中,我們建議將添加到塊內(nèi),這樣瀏覽器引擎就能夠盡早地發(fā)現(xiàn)需要解析的腳本,然后將其分配給后臺(tái)線程進(jìn)行處理。我們也可以查看 DevTools Timeline 來(lái)確定腳本是否被后臺(tái)解析,特別是當(dāng)你存在某個(gè)關(guān)鍵性腳本需要解析的時(shí)候,更需要確定該腳本是由 streaming 線程解析的。
我們同樣致力于打造更輕量級(jí)、更快的解析器,目前 V8 主線程中最大的瓶頸在于所謂的非線性解析消耗。譬如我們有如下的代碼片:
(function (global, module) { … })(this, function module() { my functions })
V8 并不知道我們編譯主腳本的時(shí)候是否需要module這個(gè)模塊,因此我們會(huì)暫時(shí)放棄編譯它。而當(dāng)我們打算編譯module時(shí),我們需要重分析所有的內(nèi)部函數(shù)。這也就是所謂的 V8 解析時(shí)間非線性的原因,任何一個(gè)處于 N 層深度的函數(shù)都有可能被重新分析 N 次。V8 已經(jīng)能夠在首次編譯的時(shí)候搜集所有內(nèi)部函數(shù)的信息,因此在未來(lái)的編譯過(guò)程中 V8 會(huì)忽略所有的內(nèi)部函數(shù)。對(duì)于上面這種module形式的函數(shù)會(huì)是很大的性能提升,建議閱讀The V8 Parser(s)?—?Design, Challenges, and Parsing JavaScript Better來(lái)獲取更多內(nèi)容。V8 同樣在尋找合適的分流機(jī)制以保證啟動(dòng)時(shí)能在后臺(tái)線程中執(zhí)行 JavaScript 編譯過(guò)程。
預(yù)編譯 JavaScript?每隔幾年就有人提出引擎應(yīng)該提供一些處理預(yù)編譯腳本的機(jī)制,換言之,開發(fā)者可以使用構(gòu)建工具或者其他服務(wù)端工具將腳本轉(zhuǎn)化為字節(jié)碼,然后瀏覽器直接運(yùn)行這些字節(jié)碼即可。從我個(gè)人觀點(diǎn)來(lái)看,直接傳送字節(jié)碼意味著更大的包體,勢(shì)必會(huì)增加加載時(shí)間;并且我們需要去對(duì)代碼進(jìn)行簽名以保證能夠安全運(yùn)行。目前我們對(duì)于 V8 的定位是盡可能地避免上文所說(shuō)的內(nèi)部重分析以提高啟動(dòng)時(shí)間,而預(yù)編譯則會(huì)帶來(lái)額外的風(fēng)險(xiǎn)。不過(guò)我們歡迎大家一起來(lái)討論這個(gè)問(wèn)題,雖然 V8 目前專注于提升編譯效率以及推廣利用 Service Worker 緩存腳本代碼來(lái)提升啟動(dòng)效率。我們?cè)?BlinkOn7 上與 Facebook 以及 Akamai 也討論過(guò)預(yù)編譯相關(guān)內(nèi)容。
Optimize JS 優(yōu)化類似于 V8 這樣的 JavaScript 引擎在進(jìn)行完整的解析之前會(huì)對(duì)腳本中的大部分函數(shù)進(jìn)行預(yù)解析,這主要是考慮到大部分頁(yè)面中包含的 JavaScript 函數(shù)并不會(huì)立刻被執(zhí)行。
預(yù)編譯能夠通過(guò)只處理那些瀏覽器運(yùn)行所需要的最小函數(shù)集合來(lái)提升啟動(dòng)時(shí)間,不過(guò)這種機(jī)制在 IIFE 面前卻反而降低了效率。盡管引擎希望避免對(duì)這些函數(shù)進(jìn)行預(yù)處理,但是遠(yuǎn)不如optimize-js這樣的庫(kù)有作用。optimize-js 會(huì)在引擎之前對(duì)于腳本進(jìn)行處理,對(duì)于那些立即執(zhí)行的函數(shù)插入圓括號(hào)從而保證更快速地執(zhí)行。這種預(yù)處理對(duì)于 Browserify, Webpack 生成包體這樣包含了大量即刻執(zhí)行的小模塊起到了非常不錯(cuò)的優(yōu)化效果。盡管這種小技巧并非 V8 所希望使用的,但是在當(dāng)前階段不得不引入相應(yīng)的優(yōu)化機(jī)制。
總結(jié)啟動(dòng)階段的性能至關(guān)重要,緩慢的解析、編譯與執(zhí)行時(shí)間可能成為你網(wǎng)頁(yè)性能的瓶頸所在。我們應(yīng)該評(píng)估頁(yè)面在這個(gè)階段的時(shí)間占比并且選擇合適的方式來(lái)優(yōu)化。我們也會(huì)繼續(xù)致力于提升 V8 的啟動(dòng)性能,盡我所能!
延伸閱讀Planning for Performance
Solving the Web Performance Crisis by Nolan Lawson
JS Parse and Execution Time
Measuring Javascript Parse and Load
Unpacking the Black Box: Benchmarking JS Parsing and Execution on Mobile Devices (slides)
When everything’s important, nothing is!
The truth about traditional JavaScript benchmarks
Do Browsers Parse JavaScript On Every Page Load
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/81500.html
摘要:譯文地址譯唯快不破應(yīng)用的個(gè)優(yōu)化步驟前端的逆襲知乎專欄原文地址時(shí)過(guò)境遷,應(yīng)用比以往任何時(shí)候都更具交互性。使用負(fù)載均衡方案我們?cè)谥坝懻摼彺娴臅r(shí)候簡(jiǎn)要提到了內(nèi)容分發(fā)網(wǎng)絡(luò)。換句話說(shuō),元素的串形訪問(wèn)會(huì)削弱負(fù)載均衡器以最佳形式 歡迎關(guān)注知乎專欄 —— 前端的逆襲歡迎關(guān)注我的博客,知乎,GitHub。 譯文地址:【譯】唯快不破:Web 應(yīng)用的 13 個(gè)優(yōu)化步驟 - 前端的逆襲 - 知乎專欄原文地...
摘要:表示調(diào)用棧在下一將要執(zhí)行的任務(wù)。兩方性能解藥我們一般有兩種方案突破上文提到的瓶頸將耗時(shí)高成本高易阻塞的長(zhǎng)任務(wù)切片,分成子任務(wù),并異步執(zhí)行這樣一來(lái),這些子任務(wù)會(huì)在不同的周期執(zhí)行,進(jìn)而主線程就可以在子任務(wù)間隙當(dāng)中執(zhí)行更新操作。 showImg(https://segmentfault.com/img/remote/1460000016008111); 性能一直以來(lái)是前端開發(fā)中非常重要的話題...
摘要:前端每周清單專注前端領(lǐng)域內(nèi)容,以對(duì)外文資料的搜集為主,幫助開發(fā)者了解一周前端熱點(diǎn)分為新聞熱點(diǎn)開發(fā)教程工程實(shí)踐深度閱讀開源項(xiàng)目巔峰人生等欄目。背后的故事本文是對(duì)于年之間世界發(fā)生的大事件的詳細(xì)介紹,闡述了從提出到角力到流產(chǎn)的前世今生。 前端每周清單專注前端領(lǐng)域內(nèi)容,以對(duì)外文資料的搜集為主,幫助開發(fā)者了解一周前端熱點(diǎn);分為新聞熱點(diǎn)、開發(fā)教程、工程實(shí)踐、深度閱讀、開源項(xiàng)目、巔峰人生等欄目。歡迎...
平日學(xué)習(xí)接觸過(guò)的網(wǎng)站積累,以每月的形式發(fā)布。2017年以前看這個(gè)網(wǎng)址:http://www.kancloud.cn/jsfron... 1. Javascript 前端生成好看的二維碼 十大經(jīng)典排序算法(帶動(dòng)圖演示) 為什么知乎前端圈普遍認(rèn)為H5游戲和H5展示的JSer 個(gè)人整理和封裝的YU.js庫(kù)|中文詳細(xì)注釋|供新手學(xué)習(xí)使用 擴(kuò)展JavaScript語(yǔ)法記錄 - 掉坑初期工具 漢字拼音轉(zhuǎn)換...
閱讀 2092·2021-11-23 09:51
閱讀 2243·2021-09-29 09:34
閱讀 3733·2021-09-22 15:50
閱讀 3584·2021-09-22 15:23
閱讀 2648·2019-08-30 15:55
閱讀 730·2019-08-30 15:53
閱讀 3104·2019-08-29 17:09
閱讀 2660·2019-08-29 13:57