PhantomJS is dead, long live headless browsers
這是一個從PhantomJs走到Headless Chrome的故事挫以,趟過了Highcharts的性能問題的坑诫惭,掉入過中文官方文檔的錯誤的坑,嘗試過依賴庫的源碼修改桩了,最后的結果卻是發(fā)現(xiàn)了Headless Chrome的新大陸附帽。
Highcharts性能問題的解決
事情得從一個bug說起,手頭的項目有一個很簡單的功能:下載PDF格式的月度報表井誉,報表里有三個scatter chart士葫,這里我們使用的是 Highcharts 去實現(xiàn)的。我們的策略是點擊下載的時候送悔,在后臺用 PhantomJs 實時生成PDF文件慢显。
一切都很完美,穩(wěn)定性和性能的表現(xiàn)都很好欠啤,直到如上圖的chart荚藻,一共有一萬多個點,整個報表有三個這樣的chart洁段,總共差不多五萬個數(shù)據(jù)點应狱,這個時候就發(fā)現(xiàn)Highcharts的性能直線下降了,結果就是報表因為超時無法下載祠丝。
我們的實現(xiàn)如下:
//report-to-pdf.js
var page = require('webpage').create();
var system = require('system');
var address = system.args[1];
var renderTo = system.args[2]
page.viewportSize = { width:1190, height:1684 };
// guarantee the charts loaded!
// https://stackoverflow.com/questions/11340038/phantomjs-not-waiting-for-full-page-load
page.onConsoleMessage = function(msg, lineNum, sourceId) {
if(msg=='hey lets take screenshot')
{
window.setInterval(function(){
try
{
var sta= page.evaluateJavaScript("function(){ return jQuery.active;}");
if(sta == 0)
{
window.setTimeout(function(){
page.render(renderTo);
clearInterval();
phantom.exit();
},1000);
}
}
catch(error)
{
console.log(error);
phantom.exit(1);
}
},1000);
}
};
page.open(address, function (status) {
if (status !== "success") {
console.log('Unable to load url');
phantom.exit();
} else {
page.setContent(page.content.replace('</body>','<script>window.onload = function(){console.log(\'hey lets take screenshot\');}</script></body>'), address);
}
});
#audit_report_controller.rb
def download
report = AuditReport.find_by(token: params[:token])
if report && report.file_path && File.exist?(report.file_path)
output_file = report.file_path
else
file_name = "REPORT_#{report.project_name}_#{report['started_at'].strftime('%Y%m')}_#{params[:token]}.pdf"
js_path = "#{Rails.root}/app/assets/javascripts/headless-chrome-pdf.js"
output_file = "#{Rails.root}/tmp/#{file_name}"
url = audit_report_preview_url(token: params[:token])
Phantomjs.run(js_path, url, output_file)
report.update({file_path: output_file})
end
send_file(output_file, :filename => file_name, :type => "application/pdf")
end
這個是我們嘗試疾呻,用瀏覽器直接打開用來生成pdf的html頁面,發(fā)現(xiàn)效率極差:
經(jīng)過調研写半,得知Highcharst確實存在效率問題岸蜗,對于幾萬個點的scatter它是無能為力的,但是好在有 解決方案叠蝇,我們可以使用 Highchart-boost module.
這一方案使用WebGL實現(xiàn)圖表璃岳,替換標準解決方案中的SVG,最終獲得了百萬點位毫秒級加載的效果悔捶。當然需要注意铃慷,這也造成了一些效果上的限制:
The boost module is a stripped down version of the SVG renderer. As such, certain features are not available for boosted charts. Most of these features deals with interactivity, such as animation support. But there are a few that relates to visuals as well.
如是我們引入了boost.js, 然后設置了seriesThreshold參數(shù)為2000,也就是當點位數(shù)量達到2000以上時候蜕该,才開啟boost 模式犁柜。在瀏覽打開預覽頁面時候數(shù)據(jù)如下:
這是在development環(huán)境下,未壓縮assets時候的速度堂淡,基本chart的渲染無障礙馋缅,實現(xiàn)了秒內加載坛怪。
Boost && PhantomJs的坑
接下來我們嘗試使用PhantomJs將預覽頁面轉化為PDF,在terminal使用命令:
phantomjs report-to-pdf.js http://dev/audit_report_preview\?token\=EeHwf7Gi3fO6S2qzfM test.pdf
果然實現(xiàn)了快速下載股囊,但是結果卻是很慘烈的袜匿,chart完全沒有render出來,chart區(qū)白茫茫的一片稚疹。WTF!
繼續(xù)查找問題的所在居灯,想一想boost module對chart的實現(xiàn)方式有什么特點,用chrome打開報表預覽頁面内狗,查看html元素怪嫌,可以看到boost.js使用了Embeded Base64 image的方式,使用WebGL畫好圖片柳沙,使用base64 encode之后嵌入頁面相應 image tag的Data url屬性岩灭,懷疑是Embeded image在PhantomJs里打開有問題,搜索了PhantomJs的github issues赂鲤,確實看到了相關問題噪径,還是個open的issue,已經(jīng)一年半了数初,同時也看到PhantomJS 浩浩蕩蕩的未解決的issues找爱,到寫下此段時候,open issues的總共數(shù)目是:1812泡孩。這讓我嗅到了一絲絕望的氣息:首先說明PhantomJs確實有渲染Embed image的問題车摄,未解決。同時這個項目可能維護狀態(tài)不佳仑鸥。
絕望這下尋求了rails里的其他的一些常用pdf 生成插件吮播,比如:wicked_pdf,pdfkit眼俊,但是結果竟然還是一樣意狠,生成的PDF里面chart區(qū)域白茫茫的一片。此時不得不考慮泵琳,真的是因為Embed Base64 image的問題摄职?還是得驗證一下問題誊役,于是在瀏覽器上copy下預覽頁面的的html获列,手動設置為report-to-pdf.js 中page的content,生成了PDF之后蛔垢,竟然發(fā)現(xiàn)chart出現(xiàn)了击孩,所以說明,白茫茫的chart的鹏漆,還真不是embed base64 image的鍋巩梢,那么可能在PhantomJs環(huán)境下创泄,可能chart根本沒有畫出來,而不是畫出來了顯示不了括蝠,就想使用如下代碼查看PhantomJs打印出來的page content:
//Javascript
var page = require('webpage').create();
var address='http://dev/audit_report_preview?token=EeHwf7Gi3fO6S2qzfMm';
page.onConsoleMessage = function(msg, lineNum, sourceId) {
console.log('I am here: ' + msg)
};
page.open(address, function (status) {
if (status !== "success") {
console.log('Unable to load url');
} else {
var content = page.content;
console.log('Content: ' + content);
}
phantom.exit();
})
結果發(fā)現(xiàn)真的是完全沒有image element鞠抑,更驚喜的是細看之下 ,打印內容第一行竟然給出了error信息:
I am here: Highcharts error #26: www.highcharts.com/errors/26
搜索了一下Highcharts的文檔忌警,error#26說的很直白了:
原來是因為PhantomJs不支持 WebGL搁拙,在谷歌上做相關搜索,結果確實如此法绵,并且開發(fā)者表示箕速,此后也不會支持WebGL。這個時候嘗試了上面文檔里給出的方案朋譬,在boost.js之后引入了boost-convas.js模塊盐茎,讓在不支持WebGL的時候fallback到canvas,但是結果依然是白茫茫的一片徙赢,再次打出content log信息字柠,竟然還是同樣的錯誤,有點懷疑人生了狡赐,這次直接點開錯誤信息給出的鏈接募谎,走到英文文檔的地址,給出的信息是:
竟然是要求把boost-canvas.js模塊放在boost.js之前引入阴汇,所以這里被坑爹中文文檔帶偏了数冬!okay,按照文檔的要求做了調整搀庶,果然不再有#26 error了拐纱,不過不幸的是chart只有數(shù)軸,但是散點圖中的點都沒有顯示出來哥倔,再次回到瀏覽器預覽頁面秸架,去做調試,在console里進入源碼咆蒿,修改代碼 禁用WebGL东抹,強制fallback到canvas,在瀏覽器上看到效果也是一樣沃测,也是沒有畫出來散點缭黔,然后看到瀏覽器console里有一些關于boost-canvas.js的報錯,是一些類似undefined的初級問題蒂破,一一針對性的修改源碼做出解決馏谨,但是在沒有報錯情況下依舊沒有畫出散點,翻看源碼附迷,想找出問題所在惧互,同時也好奇google出品的代碼質量怎么會這樣哎媚,如是懷疑是不是版本問題,發(fā)現(xiàn)自己使用的Highcharts的版本比較老喊儡,和使用的boost 以及 boost-canvas不匹配拨与,更換版本之后終于可以顯示正常的chart,性能表現(xiàn)也不錯艾猜。
PhantomJs is dead
再次回到命令行截珍,嘗試服務端PhantomJs生成PDF,結果卻是大跌眼鏡箩朴,瀏覽器端的飛速性能又沒了岗喉,依舊是分鐘級的時間花費才完成生成動作。此時再回頭去求助google炸庞,以及翻看PhantomJs的issues钱床,看到很多類似的情況,在PhantomJs中使用canvas的效率低下問題埠居,page.open()在面對體量比較大的頁面的時候查牌,效率問題也一直為人所詬病,最后此類討論給出的結論都是 效率問題在PhantomJs中是無解的滥壕,只能去求助別的方案纸颜。最讓人傷心的是看到了這條issue: Archiving the project: suspending the development,創(chuàng)始人表示PhantomJs已經(jīng)結束開發(fā)了绎橘。想起之前嘗試幾種rails的gem生成PDF方案胁孙,效果都不好,感覺開發(fā)陷入了困境称鳞。
拯救者Headless Chrome
柳暗花明又一村涮较,無意中翻看關于PhantomJs 性能問題的各種相關文章的時候看到 benchmark headless chrome vs phantomjs,想到為啥不去嘗試一下Headless Chrome方案呢冈止,按照官方文檔做起來嘗試狂票,幾分鐘后發(fā)現(xiàn)竟然在這么一條命令下面,完美的解決了問題熙暴!
chrome --headless --print-to-pdf=file.pdf http://localhost:3000/dev\?token\=EeHwf7Gi3fO6S2qzfMm
畫面渲染正常闺属,chart正常,效率也很好周霉,接下來需要做的就是將Headless Chrome的方案和服務端Rails項目集成的問題:第一個掂器,服務器系統(tǒng)是linux,必須調研下安裝使用的問題诗眨,第二個是在Rails里調用的話唉匾,是否有成熟穩(wěn)定的方案。
現(xiàn)在有穩(wěn)定版本的Node API可以使用:
Puppeteer is a Node library which provides a high-level API to control headless Chrome or Chromium over the DevTools Protocol. It can also be configured to use full (non-headless) Chrome or Chromium.
于是對于第一個問題匠楚,默認安裝的時候巍膘,Puppeteer會一起默認下載安裝一個 Chromium內核,同時需要注意對于不同發(fā)型版的一些依賴庫芋簿,我們這里使用的是Ubuntu峡懈,可以提前安裝好依賴,再安裝Puppeteer:
sudo apt-get install gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
npm instal puppeteer
對于第二個問題与斤,找到一篇guidline肪康,這里的方案是將我們自己的Rails項目文件夾初始化為一個node項目,也就是生成一個package.json和node_module文件夾做依賴管理撩穿,這樣在部署或者團隊開發(fā)中磷支,只要有node環(huán)境和npm install一下就可以正常使用node api的相關功能,然后在Rails代碼中是調用命令行node命令的方式執(zhí)行。按照這個方案做了集成,開發(fā)環(huán)境下嘗試功能沒有問題仑性,剩下的考慮怎么部署問題队橙。我們使用的是mina,在deploy.rb中加入以下任務模塊:
namespace :npm do
desc 'Install node modules using Npm.'
task install: :environment do
queue %{echo "-----> Installing node modules using Npm"}
queue "mkdir -p #{deploy_to}/#{shared_path}/node_modules"
queue "ln -s #{deploy_to)}/#{shared_path}/node_modules" "node_modules"
queue "npm install"
end
end
...
deploy do
# Put things that will set up an empty directory into a fully set-up
# instance of your project.
....
invoke :'bundle:install'
invoke :'npm:install'
invoke :'rails:db_migrate'
invoke :'rails:assets_precompile'
invoke :'deploy:cleanup'
....
end
end
PhantomJS is dead, long live headless browsers
最后咕别,填好坑回到題記的話題,這幾年各種headless的browser崛起,讓老一代的server端的解決方案失去了市場移盆,包括 PhantomJS。Selenium IDE for Firefox 等伤为,隨著 PhantomJs的Maintainer Vitaly Slobodin 宣布不再維護該項目咒循,標志PhantomJs的時代已經(jīng)遠去,使用Headless Chrome等新一代的解決方案有如下優(yōu)點:
- They are real browsers with a broad feature support (PhantomJS uses a very old version of WebKit – and in the meanwhile Chrome switched to Blink anyway)
- They are faster and more stable (PhantomJS has a lot of open issues)
- They use less memory
- They can be started non-headless, which allows easier debugging
- No more goofy PhantomJS binary installation with NPM
這樣绞愚,我們也可以入了Headless Chrome的坑了剑鞍,可以用來當做爬蟲,用來作為測試的Javascript driver方案等爽醋。
- Generate screenshots and PDFs of pages.
- Crawl a SPA and generate pre-rendered content (i.e. "SSR").
- Automate form submission, UI testing, keyboard input, etc.
- Create an up-to-date, automated testing environment. Run your tests directly in the latest version of Chrome using the latest JavaScript and browser features.
- Capture a timeline trace of your site to help diagnose performance issues.
歡迎入坑蚁署!