改善 Wikipedia 的 JavaScript,減少 300ms 的 blocking time

Hacker News 首頁上看到「300ms Faster: Reducing Wikipedia's Total Blocking Time」這篇,作者 Nicholas RayWikimedia Foundation 的工程師,雖然是貼在自己的 blog 上,但算是半官方性質了... 文章裡面提到了兩個改善都是跟前端 JavaScript 有關的。

作者是透過瀏覽器端的 profiling 產生火焰圖,判讀裡面哪塊是大塊的問題,然後看看有沒有機會改善。

先看最後的成果,可以看到第一個 fix 讓 blocking time 少了 200ms 左右,第二個 fix 則是少了 80ms 左右:

第一個改善是從火焰圖發現 l._enable 吃掉很多 blocking time:

作者發現是因為 find() 找出所有的連結後 (a 元素),跑去每一個連結上面綁定事件造成的效能問題:

The .on("click") call attached a click event listener to nearly every link in the content so that the corresponding section would open if the clicked link contained a hash fragment. For short articles with few links, the performance impact was negligible. But long articles like ”United States” included over 4,000 links, leading to over 200ms of execution time on low-end devices.

但這其實是 redundant code,在其他地方已經有處理了,所以解法就比較簡單,拔掉後直接少了 200ms:

Worse yet, this behavior was unnecessary. The downstream code that listened to the hashchange event already called the same method that the click event listener called. Unless the window’s location already pointed at the link’s destination, clicking a link called the checkHash method twice — once for the link click event handler and once more for the hashchange handler.

第二個改善是 initMediaViewer 吃掉的 blocking time,從 code 也可以看到問題也類似,跑一個 loop 把事件掛到所有符合條件的元素上面:

這邊的解法是 event delegation,把事件掛到上層的元素,就只需要掛一個,然後多加上檢查事件觸發的起點是不是符合條件就可以了,這樣可以大幅降低「掛」事件的成本。

這點算是常用技巧,像是 table 裡面有事件要掛到很多個 td 的時候,會改成把一個事件掛到 table 上面,另外加上判斷條件。

算是蠻標準的 profiling 過程,直接拉出真實數據來看,然後調有重大影響的部分。

用 Fly.io 跑 RSS-Bridge,再把現有的 twitter2facebook 與 twitter2plurk 改寫

Twitter 把我本來 read-only 的兩個應用程式停用掉了,加上這陣子的新聞,就改用其他方式來處理。

用的是先前在「用 RSS-Bridge 接服務」提到的 RSS-Bridge,可以將 Twitter 的資料轉成 JSON Feed

其中 RSS-Bridge 是 PHP 寫的,剛好就拿先前在「在 Fly.io 上面跑 PHP」這邊提到的方法丟上 Fly.io,不需要自己架主機跑了。

然後把 twitter2facebooktwitter2plurk 這兩個專案裡面本來抓 Twitter API 的程式碼改成抓 JSON Feed。

先這樣子弄,之後再看看要不要搬...

nginx 開始嘗試支援 HTTP/3

Hacker News 上看到「Nginx 1.25.0: experimental HTTP/3 support (nginx.org)」這則消息,從 nginx 1.25.0 開始可以用 HTTP/3

HTTP/2 最大的差異就是從以往的 TCP 改到 UDP 上了,這是基於 QUIC 的經驗弄出來的東西...

nginx 的支援算是等了一陣子了,不過沒有當初 HTTP/1.1SPDY 的進步這麼明顯,我自己就沒有跟的那麼緊了。

這樣以後 office firewall 預設應該會再開 443/udp?

用 Atom feed 訂閱 crt.sh 的 Certificate Transparency

本來打算自己寫個程式訂閱 Certificate Transparency 的資料,結果發現 crt.sh 本身就有提供 Atom feed

舉我的例子「crt.sh | gslin.org」來說,對應的 Atom feed 就在 https://crt.sh/atom?q=gslin.org 這邊。

不過 crt.sh 的速度不快,我在使用 Slack 內建的 /feed add 加了很多次才成功,但設定好之後放著收更新就可以了,看起來還算方便。

官方應該是可以把 feed 的筆數壓低,降低對後端 PostgreSQL 的壓力才對,像是只丟最近 30 天的量就好,現在看起來是所有的 entry 都轉成 Atom feed 丟出來...

在 Fly.io 上面跑 PHP

Heroku 把 free tier 拔的差不多後 (「Heroku 公佈了廢止免費方案的時間表」、「Heroku 的替代方案」),大家手上的小專案都往其他的服務跑,目前看起來做的比較有規模的就是 Fly.io 了,一個人可以建很多個 organization,而每個 organization 都有 free quota 可以用...

Fly.io 官方的文件 FLy.io Docs 裡面可以看到說明,介紹怎麼把 Laravel 站台跑起來,不過沒介紹怎麼跑純 PHP 站台,所以就看了文件研究看看,發現可以用 Docker container 跑,那就簡單了。

專案放在 GitHubgslin/fly-vanilla-php 上面。

一開始我的 Fly App 用的是 V1 的版本,是跑起來了,但後來還是換到 V2 跑,雖然兩者用起來沒有太大區別 (參考「Fly Apps」這邊的說明),但畢竟官方打算都把 V1 掛上 legacy 了,新的專案就儘量別用了...

另外一開始用 buildpacks 編 Docker image,但發現太慢了,就還是去 Docker Hub 上找個大戶人家包好的 image 來用。

首先是 fly.toml 這個檔案,這邊就直接指定用 Dockerfile 來編。

然後是 Dockerfile 這個檔案,這邊用的 image 是 richarvey/nginx-php-fpm 這包,預設會開在 port 80,所以 fly.toml 裡面就把 internal_port 指定在 80

另外就是指定了 WEBROOT,我把 root 放在 public/ 下面。

跑起來以後就可以用他提供的網址測試了,我這個專案在 https://hidden-river-325.fly.dev/ 這邊可以看到,另外我有設定自己的 domain,在 https://test-fly.gslin.com/ 也可以看到一樣的 phpinfo(); 資訊。

這邊有個小插曲,我想要掛自己的 domain 上去跑 HTTPS,但如果機器沒有掛 IPv6 address 的話,Fly.io 的系統不會認定設定已經完成,也就不會往下去申請 Let's Encrypt 的憑證,我放了一天覺得奇怪,摸了辦天才發現這個 "feature"。

瀏覽器裡同一個節點上 JavaScript 的事件觸發順序

瀏覽器裡 JavaScript 的事件觸發順序是先 capture 再 bubble,這個在「Event order」這邊就有一些歷史解釋,IE8 以前只有 capture 模式,到了 IE9+ 才支援,在「Event API: bubbles」這邊也可以看到。

但如果是同一個節點上面的事件觸發順序 (假設同樣是 capture 或是同樣是 bubble),在「Are event handlers in JavaScript called in order?」這邊有些整理資料。

2000 年的「Document Object Model (DOM) Level 2 Events Specification」這邊提到沒有定義順序:

When the event reaches the target, any event listeners registered on the EventTarget are triggered. Although all EventListeners on the EventTarget are guaranteed to be triggered by any event which is received by that EventTarget, no specification is made as to the order in which they will receive the event with regards to the other EventListeners on the EventTarget.

在早期的 draft「Document Object Model (DOM) Level 3 Events Specification」裡面可以看到:

Next, the implementation must determine the current target's candidate event listeners. This must be the list of all event listeners that have been registered on the current target in their order of registration. [HTML5] defines the ordering of listeners registered through event handler attributes.

但在最新的「UI Events」(要注意這還是 draft,在 2016 年更新的) 則是拿掉了這段。

所以在設計架構時,正常還是得保守的假設沒有保證執行順序...

跑 ArchiveTeam Warrior

Archive Team 是一個致力於保存數位資料的組織,而 ArchiveTeam Warrior 則是他們提供的軟體,可以讓你很方便直接跑 worker 加入他們的 cluster,幫忙抓資料並且保存到 Internet Archive 上。

他們提供三種方法跑 ArchiveTeam Warrior,第一種是 VM 的方式,文件裡面有介紹怎麼用 VirtualBox 或是 VMware Player 跑起來。

第二種與第三種都是 container 類的方式,DockerPodman 都能跑起來。

跑起來後可以連進 http://127.0.0.1:8001/,然後選擇想要加入的項目,或是接受指令選擇目前團隊主打的項目。

在「Projects」這頁可以看到目前主力是備份 Enjin 上的資料。

丟了兩台 VPS 的機器上去用 Docker 跑,CPU 使用率看起來很低,但網路流量看起來會因為所在的地點而差蠻多的,一台大約是 300KB/sec 到 400KB/sec,換算後大約是 1TB/mo,另外一台則只有 1/10 的量。

Imgur 改變使用條款,把 Imgur 的圖片都搬回本機上

Hacker News 上看到 Imgur 的使用者條款改變的消息:「Imgur will ban explicit images on its platform this month」,在 TechCrunch 文章標題提到的東西對 blog 影響不大,反倒是公告裡面另外提到的事情比較傷。

Imgur 在「Imgur Terms of Service Update [April 19, 2023]」這邊提到了:

Our new Terms of Service will go into effect on May 15, 2023. We will be focused on removing old, unused, and inactive content that is not tied to a user account from our platform as well as nudity, pornography, & sexually explicit content.

所以很少被存取的內容也會有機會被移除掉,這導致一堆小的 blog 或是 forum 用到的內容也會爛掉。

所以決定先搬出來,掃了一下 WordPress 資料庫裡面的內容,把檔案先拉下來,弄個 CloudFront 擋在前面 (有 free quota 的關係),然後把資料庫裡面的連結整批換掉。

另外是新的內容要丟哪裡,所以用 PHP 寫了一個很簡單的 self-hosted image server,程式碼在 GitHub 上面可以翻到:「i.gslin.com」。

裡面除了 PHP 以外,也練了一下 javascript,收 paste 事件把 image/png 的資料用 fetch() 傳到 server 端處理。

現在功能還很陽春,但至少能開始用,之後再逐步加功能上去。等功能變多變複雜之後,可能會用 Composor 掛套件上去... 但現在還算簡單,一個 upload.php 處理所有事情就好。

修改 booking.com 的 dark pattern

Hacker News Daily 上看到修改 booking.comdark pattern 的套件:「De-Stressing Booking.com (alexcharlton.co)」,原文連結到「De-stressing Booking.com」,這是 2019 的文章,在介紹他寫的套件。

裡面講的是像這樣的東西:

這個例子裡面是故意用有壓力的顏色 (這邊是紅色) 去推動使用者趕快下單,算是蠻經典的 dark pattern,作者有舉個 Airbnb 的類似例子,比較起來就好很多:

在 comment 也有人提到其他種類的 dark pattern,故意把一些飯店標成已經售出,製造你不趕快訂就會釘不到的假象。不過下面有人提到,在有些法律制度比較完整的國家裡面,這會牽扯到不實宣傳之類的行為:

After browsing hotels for some time I've seen booking.com show several hotels start to sell out of rooms. That usually causes me to hurry up and book, but after several hotels showed full at once I got suspicious and checked my partners phone. The hotels still showed as available there. Dark stuff. Their website is otherwise pretty good though and I still use them.

在「Online hotel booking」這邊有英國對這些線上訂房網站的調查與裁罰。

然後在 Hacker News 上的 comment 有看到一個有趣的方法,是 PresidentObama 提到的方法 (這 id XDDD),用 uBlock Origin 來擋:

From the last time booking.com was discussed I picked up some ublock origin filters that make the website more bearable.

You can copy and paste them directly in your ublock config (ublock options -> My filters)

  ! https://news.ycombinator.com/item?id=21860328
  booking.com##.soldout_property
  booking.com##.sr_rooms_left_wrap.only_x_left
  booking.com##.lastbooking
  booking.com##.sr--x-times-booked
  booking.com##.in-high-demand-not-scarce
  booking.com##.top_scarcity
  booking.com##.hp-rt-just-booked
  booking.com##.cheapest_banner_content > *
  booking.com##.hp-social_proof
  booking.com##.fe_banner__red.fe_banner__w-icon.fe_banner__scale_small.fe_banner
  booking.com##.urgency_message_x_people.urgency_message_red
  booking.com##.rackrate
  booking.com##.urgency_message_red.altHotels_most_recent_booking
  booking.com##.fe_banner__w-icon-large.fe_banner__w-icon.fe_banner
  booking.com##.smaller-low-av-msg_wrapper
  booking.com##.small_warning.wxp-sr-banner.js-wxp-sr-banner
  booking.com##.lock-price-banner--no-button.lock-price-banner.bui-u-bleed\@small.bui-alert--large.bui-alert--success.bui-alert

另外還有擋一些追蹤的 url parameter:

Apart from these, I use some additional ublock filters to block some of their tracking that I am not ok with.

$removeparam=/^(error_url|ac_suggestion_theme_list_length|ac_suggestion_list_length|search_pageview_id|ac_click_type|ac_langcode|ac_position|ss_raw|from_sf|is_ski_area|src|sb_lp|sb|search_selected|srpvid|click_from_logo|ss|ssne|ssne_untouched|b_h4u_keep_filters|aid|label|all_sr_blocks|highlighted_blocks|ucfs|arphpl|hpos|hapos|matching_block_id|from|tpi_r|sr_order|srepoch|sr_pri_blocks|atlas_src|place_types)/,domain=booking.com
  $removeparam=/sid=.\*;BBOX/,domain=booking.com
  ||www.booking.com/c360/v1/track
  ||www.booking.com/fl/exposed
  ||booking.com/personalisationinfra/track_behaviour_property
  ||booking.com/has_seen_review_list

不過好像很少用 booking.com 了...

用 RSS-Bridge 接服務

查資料的時候發現 RSS-Bridge 這個用 PHP 寫的專案,直接找個 PHP hosting 架起來就可以用了,沒有什麼其他的需求。

簡單架起來測了一輪,看起來不賴啊,如果一般人要用的話可以考慮就用這個專案就好,量很少的人可以用官方列出來的 Public instances 玩一下,量多的人可以自己架,PHP hosting 還蠻好找的,官方要求要 7.4+,注意一下 PHP hosting 提供的版本應該不會有太多問題。

自己寫的 feedgen 比較偏順便練 Python,不過當時的確是還不知道有這樣的專案,看了一下 GitHub 上的 tag 記錄,2013 就有的專案...