MutationObserver中用document.write致浏览器加载停滞的原因及方案问询
免责声明
我已了解为何应避免使用document.write以及document.write会清空页面的相关内容。
需求场景
需要在DOM加载阶段(document.readyState = 'loading'),在某一特定现有同步脚本之前,注入一个**同步(阻塞型)**脚本到DOM中。
可编辑HTML时的实现方案
若有权限编辑HTML代码,可通过如下方式实现:
<html> <head> <title>It's okay</title> <script> document.write( '<script src="https://www.googletagmanager.com/gtm.js?id=GTM-WZNT8QT" onload="console.log(1)"><'+'/script>' ); </script> <script src="https://code.jquery.com/jquery-3.6.3.min.js" onload="console.log(2)"></script> </head> <body> <script>console.log('DOM is parsed')</script> </body> </html>
该实现可正常加载两个JS文件,控制台依次输出1、2、DOM is parsed。
使用MutationObserver时的异常情况
但尝试通过MutationObserver实现(检测新脚本节点插入但尚未执行的时机)时:
<html> <head> <title>Why not loading?</title> <script> // 假设此JS由浏览器扩展在HTML解析前注入 new MutationObserver((mutationList) => { for (const mutation of mutationList) { for (const node of mutation.addedNodes) { if (node.tagName !== 'SCRIPT' || !node.src.includes('jquery')) { continue; } document.write( '<script src="https://www.googletagmanager.com/gtm.js?id=GTM-WZNT8QT" onload="console.log(1)"><'+'/script>' ); } } }).observe(document, { childList: true, subtree: true }); </script> <script src="https://code.jquery.com/jquery-3.6.3.min.js" onload="console.log(2)"></script> </head> <body> <script>console.log('Now DOM is parsed!')</script> </body> </html>
出现异常:浏览器持续处于加载状态,控制台从未输出2和Now DOM is parsed!;检查实际HTML代码,head中仅存在googletagmanager脚本;JavaScript线程未阻塞,但document.readyState始终停留在'loading'状态,页面功能完全失效。

问题
- 为何会出现此情况?如何解释?我原本预期两个脚本都会被执行。
- 在无法编辑HTML但可在HTML解析前注入JS(如浏览器扩展)的场景下,是否有方法在指定脚本执行前加载同步代码?
注:实际使用MutationObserver时,我至少可通过在回调中执行node.setAttribute('src', 'text/prevented')阻止jquery代码运行,但我想理解为何原方案无法生效。
解答
问题1:异常原因解释
HTML解析器遇到同步脚本时会暂停解析,先执行该脚本。而MutationObserver的回调是异步触发的——当监听器检测到jQuery脚本节点被插入时,HTML解析器已经进入了执行该脚本的准备流程。
此时在异步回调中调用document.write,浏览器会将其视为“结束当前文档解析并写入新内容”,而非同步脚本执行期间的“插入到解析流中”行为。这直接清空了原本的jQuery脚本和后续DOM内容,导致浏览器陷入无限等待解析完成的状态,readyState一直停留在loading,后续代码自然无法执行。
问题2:可行的替代方案
在无法编辑HTML但能提前注入JS的场景下,有两种可靠方式实现同步脚本前置加载:
重写
document.createElement拦截脚本创建
在HTML开始解析前,重写document.createElement方法,检测到目标脚本节点(如包含jquery的src)时,先同步加载所需脚本,再允许原脚本创建。示例代码:const originalCreateElement = document.createElement; document.createElement = function(tagName) { if (tagName.toLowerCase() === 'script') { const script = originalCreateElement.call(this, tagName); const originalSetAttribute = script.setAttribute; script.setAttribute = function(name, value) { if (name === 'src' && value.includes('jquery')) { // 在同步上下文写入前置脚本,避免文档覆盖 document.write('<script src="https://www.googletagmanager.com/gtm.js?id=GTM-WZNT8QT" onload="console.log(1)"><'+'/script>'); } originalSetAttribute.call(this, name, value); }; return script; } return originalCreateElement.call(this, tagName); };这种方式在目标脚本节点创建的同步流程中注入前置脚本,不会触发文档覆盖。
浏览器扩展用
webRequestAPI拦截请求
如果是浏览器扩展场景,可通过webRequestAPI拦截目标脚本(如jQuery的URL)请求,先同步加载前置脚本,再允许目标脚本请求继续。这种网络层面的拦截能确保前置脚本在目标脚本执行前加载完成,且不影响DOM解析流程。
内容的提问来源于stack exchange,提问作者nikitaeverywhere

