浏览器解析HTML头部的底层逻辑:揭秘doctype、charset、meta标签如何影响HTML解析器与渲染管线
序章:从网络字节流到像素的奇幻漂流
当你在浏览器地址栏输入一个网址并按下回车,到最终看到完整的页面呈现在屏幕上,这个过程涉及数十个组件、数百万行代码的协同工作。而这一切的起点,是一个看似简单却蕴含深意的文本:HTML。
HTML文档的开头部分——即所谓的"头部"(head),包含了doctype声明、字符集定义、各种meta标签等。这些内容不直接展示给用户,但它们如同交响乐的指挥家,默默指挥着浏览器解析器、渲染引擎的工作方式。本文将深入剖析这些头部标签的底层逻辑,带你走进浏览器引擎的内部世界。
让我们从一个关键问题开始:浏览器如何将一段文本字符串,转换成包含样式、交互、布局的完整页面?答案隐藏在HTML解析器与渲染管线的协作机制中。
第一章:Doctype——渲染模式的总开关
1.1 历史背景:从混沌到标准
在Web发展早期,浏览器厂商各自为政,IE和Netscape实现了不同的渲染逻辑。当W3C标准出现后,浏览器需要同时支持两种页面:遵循标准的"现代页面"和针对旧浏览器设计的"遗留页面"。这就产生了渲染模式的区分。
Doctype声明的本质是一个指令,告诉浏览器使用哪种引擎模式来渲染页面。这不是一个可有可无的修饰,而是直接影响布局、盒模型、脚本行为的决定性因素。
1.2 三种渲染模式
浏览器内核内部维护着一个状态变量——document.compatMode。根据doctype的不同,这个变量会被设置为以下三种值之一:
标准模式(Standards Mode):
-
document.compatMode === "CSS1Compat" -
浏览器按照W3C标准解析CSS和布局
-
使用标准的盒模型:宽度=内容宽度,padding和border额外增加
怪异模式(Quirks Mode):
-
document.compatMode === "BackCompat" -
模拟旧浏览器(IE5及以下)的非标准行为
-
使用IE盒模型:宽度=内容宽度+padding+border
-
表格字体继承、盒模型尺寸、行高计算等都与标准模式不同
近乎标准模式(Almost Standards Mode):
-
只有少数几处行为与标准模式不同(主要是表格单元格中图片的垂直对齐方式)
1.3 Doctype触发的模式切换逻辑
浏览器解析器在读取HTML字节流的最开始(甚至在<html>标签之前),会检查是否存在doctype声明。以下是各大浏览器引擎(WebKit/Blink、Gecko、EdgeHTML)共同遵循的模式切换规则:
触发标准模式的doctype:
html
<!DOCTYPE html> <!-- HTML5 doctype,最简单,触发标准模式 --> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
触发近乎标准模式的doctype:
html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
触发怪异模式的情况:
-
没有doctype声明
-
doctype声明不完整或格式错误(如
<!DOCTYPE html PUBLIC "something">缺少URL) -
doctype声明位置不在文档最开头(前面有注释、空格或文本节点)
-
使用了已知会触发怪异模式的旧doctype(如HTML 3.2的doctype)
1.4 Blink引擎中的模式判定源码分析
Chromium(Blink引擎)中判定渲染模式的核心逻辑位于html_parser.cc或Document.h中。简化后的判定逻辑如下:
cpp
// 伪代码,展示核心逻辑
Document::CompatibilityMode DetermineCompatibilityMode(Document* document) {
// 获取doctype节点
DocumentType* doctype = document->doctype();
if (!doctype) {
// 没有doctype -> 怪异模式
return CompatibilityMode::kQuirksMode;
}
const String& name = doctype->name();
const String& publicId = doctype->publicId();
const String& systemId = doctype->systemId();
// 检查是否为HTML5 doctype
if (name == "html" && publicId.empty() && systemId.empty()) {
return CompatibilityMode::kNoQuirksMode;
}
// 检查标准模式(Strict DTD)
if (name == "HTML" &&
publicId == "-//W3C//DTD HTML 4.01//EN" &&
systemId == "http://www.w3.org/TR/html4/strict.dtd") {
return CompatibilityMode::kNoQuirksMode;
}
// 检查Transitional DTD
if (name == "HTML" &&
publicId == "-//W3C//DTD HTML 4.01 Transitional//EN") {
if (systemId.empty() || systemId == "http://www.w3.org/TR/html4/loose.dtd") {
return CompatibilityMode::kLimitedQuirksMode; // 近乎标准
}
}
// 其他各种历史doctype判定...
// 最终默认返回怪异模式
return CompatibilityMode::kQuirksMode;
}
1.5 模式差异的具体表现
盒模型差异:
css
/* 标准模式:宽高不包含padding/border */
.box { width: 100px; padding: 10px; } /* 实际占据120px宽度 */
/* 怪异模式:宽高包含padding/border */
.box { width: 100px; padding: 10px; } /* 实际占据100px宽度,内容区只剩80px */
其他关键差异:
-
内联元素的高度:怪异模式下inline元素可设置高度
-
表格单元格的vertical-align:怪异模式默认为
middle -
字体继承:怪异模式下表单元素不自动继承body字体
-
选择器优先级:部分伪类的计算方式不同
-
盒溢出行为:
overflow: visible的差异处理
1.6 底层解析器的处理时机
HTML解析器(如Blink的HTMLDocumentParser)在创建Document对象后立即检查doctype。这个时机非常关键:在开始解析body之前,渲染模式就已经确定。这意味着无法通过JavaScript动态改变document.compatMode——它是一个只读的、在解析初始化阶段就固定的属性。
第二章:字符编码(Charset)——从字节到字符的解码之旅
2.1 问题本质:字节流的编码识别
当浏览器通过网络接收HTML文件时,收到的是一串二进制字节流。例如,"Hello世界"在UTF-8编码下可能是:
text
48 65 6C 6C 6F E4 B8 96 E7 95 8C
浏览器需要知道如何将这些字节正确解码成Unicode字符。选错编码会导致乱码,例如把UTF-8字节用GBK解码会显示出完全不同的字符。
2.2 编码识别的优先级链(Layer Caching)
浏览器内部实现了一套复杂的编码嗅探算法,遵循以下优先级顺序(从高到低):
-
传输层编码(HTTP Content-Type头部的charset参数)
text
Content-Type: text/html; charset=utf-8
-
BOM字节顺序标记(Byte Order Mark)
-
UTF-8 BOM: EF BB BF
-
UTF-16LE BOM: FF FE
-
UTF-16BE BOM: FE FF
-
UTF-32LE BOM: FF FE 00 00
-
UTF-32BE BOM: 00 00 FE FF
-
-
HTML内部的meta标签(
<meta charset>或<meta http-equiv="Content-Type">)html
<meta charset="utf-8"> <!-- 或旧式写法 --> <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-
解析器启发式嗅探(根据页面内容推断)
-
扫描常见的编码模式
-
统计字节分布特征(如UTF-8的字节模式有特定规律)
-
-
默认编码(浏览器或系统区域设置)
-
例如Western European (Windows-1252)
-
2.3 解码时机与解析器的状态机
HTML解析器并非等待全部字节接收完毕才开始工作,而是采用流式处理。这导致了编码处理上的复杂性:
初始阶段(无编码信息):
当解析器接收第一批字节时,如果没有BOM、没有HTTP头编码信息,它处于"编码待定"状态。此时解析器会使用一个临时编码(通常是UTF-8或Windows-1252)试探性解码。
解析头部的特殊逻辑:
解析器在解析<head>内的前512字节时,会特别留意寻找meta charset标签。一旦找到,如果还没有确定最终编码,且新编码与当前试探编码不同,解析器需要:
-
停止当前解析(部分实现会丢弃已解析的token)
-
重新初始化解码器(使用新编码)
-
重新解析从起始位置到当前位置的所有字节
这个"重新解析"操作的代价很高,因此规范建议将meta charset标签放在head开头,最好在<title>之前,确保在解析512字节内被找到。
2.4 WebKit/Blink编码嗅探算法的实现
Chromium中负责编码检测的核心组件是TextResourceDecoder类。其简化逻辑:
cpp
// 伪代码:编码检测流程
TextResourceDecoder::Encoding DetermineEncoding() {
// 1. HTTP头优先
if (m_encodingFromHTTPHeader.isValid())
return m_encodingFromHTTPHeader;
// 2. 检查BOM
if (HasBOM(m_buffer)) {
return GetEncodingFromBOM(m_buffer);
}
// 3. 搜索meta charset(仅限前1024字节)
Encoding metaEncoding = ScanForMetaCharset(m_buffer, 1024);
if (metaEncoding.isValid()) {
// 验证编码是否为可执行的(某些编码可能导致安全漏洞)
if (IsValidEncoding(metaEncoding)) {
return metaEncoding;
}
}
// 4. 启发式检测(使用ICU的编码检测器)
if (ShouldPerformHeuristicDetection()) {
Encoding detectedEncoding = DetectEncodingByHeuristics(m_buffer);
if (detectedEncoding.isValid())
return detectedEncoding;
}
// 5. 返回默认编码(通常是Latin-1/Windows-1252)
return m_defaultEncoding;
}
2.5 解码器内部状态机
字节解码器(如UTF-8解码器)是一个有限状态机,处理字节流时需要维护状态:
text
UTF-8解码状态机: - 状态0:等待ASCII字节或UTF-8序列起始字节 - 状态1:已读1个续字节(3字节序列的第二字节) - 状态2:已读2个续字节(3字节序列的第三字节) - 状态3:4字节序列的处理状态
如果解码器突然切换编码(如从UTF-8切换到GBK),这个状态机需要完全重置,之前的解码结果作废。
2.6 乱码产生的底层原因
常见的乱码场景及其原理:
场景1:GBK编码显示为UTF-8
-
原因:编码被误判为UTF-8,UTF-8解码器遇到不符合UTF-8规则的字节序列(GBK编码的字节模式与UTF-8的要求不同)
-
结果:UTF-8解码器将无效字节替换为U+FFFD(�),或者产生字符拼接错误
场景2:UTF-8 BOM导致的空格或隐形字符
-
UTF-8 BOM(EF BB BF)在某些旧渲染器中被渲染为可见字符或零宽空格
-
现代浏览器会正确处理BOM并忽略其作为显示内容
第三章:Meta标签——元数据的多功能调度器
3.1 Meta标签的分类体系
meta标签根据其作用机制分为四大类:
-
charset编码声明(已在前文详述)
-
http-equiv状态指令(模拟HTTP头)
-
name属性(文档级元数据)
-
自定义meta(如Open Graph、Twitter Card)
3.2 http-equiv的核心指令及处理逻辑
http-equiv的命名源于其功能:相当于一个等效于HTTP响应头的指令。浏览器接收到这些指令后,会采取与处理同名HTTP头相同的动作。
常见http-equiv指令及处理时机:
Content-Type:
html
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
处理逻辑:如果HTTP头中没有指定charset,此指令会被视为编码声明的备选方案。解析器在扫描meta时识别并应用。
X-UA-Compatible:
html
<meta http-equiv="X-UA-Compatible" content="IE=edge; chrome=1">
处理逻辑(IE/Edge特有):
-
告知IE以最高可用模式渲染
-
在Chromium中已被忽略
refresh:
html
<meta http-equiv="refresh" content="5; url=https://example.com">
处理逻辑:
-
解析content值,提取延迟秒数和目标URL
-
启动定时器,到期后触发导航
-
对于刷新到相同URL的实现,需要防止无限循环
Content-Security-Policy:
html
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
处理逻辑:
-
由CSP解析器处理
-
必须在
<meta charset>之后出现 -
不能通过meta设置需要用户代理唯一策略的部分(如
sandbox)
3.3 Name属性的关键应用及处理逻辑
viewport:
html
<meta name="viewport" content="width=device-width, initial-scale=1.0">
这是移动端最重要的meta标签,直接影响布局视口(layout viewport)和视觉视口(visual viewport)。其处理逻辑位于渲染引擎的Viewport类中:
cpp
// Chromium ViewportParser的简化逻辑
void ViewportParser::ProcessMetaViewport(const String& content) {
ViewportDescription description;
// 解析content字符串(格式:key=value,逗号分隔)
for (auto& token : ParseViewportTokens(content)) {
if (token.key == "width") {
if (token.value == "device-width") {
description.min_width = LayoutUnit::FromDeviceWidth();
description.max_width = LayoutUnit::FromDeviceWidth();
} else {
int width = token.value.toInt();
description.min_width = LayoutUnit(width);
description.max_width = LayoutUnit(width);
}
} else if (token.key == "initial-scale") {
description.zoom = token.value.toFloat();
} else if (token.key == "user-scalable") {
description.user_zoom = (token.value == "yes" || token.value == "1");
}
// ... 处理maximum-scale, minimum-scale等
}
// 将description应用到页面视口
page_->SetViewportDescription(description);
}
viewport的影响机制:
-
改变CSS像素与设备像素的比例关系
-
影响
window.innerWidth/innerHeight -
改变媒体查询中
device-width的值
format-detection:
html
<meta name="format-detection" content="telephone=no, email=no, address=no">
处理逻辑:
-
禁止浏览器自动识别电话号码、邮箱等并添加链接
-
Safari和Chromium各有实现,但核心逻辑是在DOM构建后,对文本节点进行后处理,跳过自动链接化
theme-color:
html
<meta name="theme-color" content="#317EFB">
处理逻辑(移动端Chrome、Firefox):
-
通知浏览器地址栏/状态栏的背景色
-
通过跨进程通信(Android Chrome中Browser进程与Renderer进程通信)更新UI
3.4 Meta标签对解析器流控的影响
meta标签本身不阻塞解析过程(除非包含csp等需要立即生效的策略),但某些meta标签会影响后续资源的加载行为:
Content-Security-Policy的影响:
-
在解析到此meta标签之前,解析器加载的资源不受CSP限制
-
因此必须将CSP meta标签放在head尽可能靠前的位置
-
CSP对
<script>、<link>、<img>等资源加载器进行过滤
referrer策略的影响:
html
<meta name="referrer" content="strict-origin">
-
在解析到此标签后,后续所有请求的Referer头按策略发送
-
已发出的请求不受影响
第四章:HTML解析器的内部构造与状态转换
4.1 解析器的多阶段流水线
现代浏览器的HTML解析器并非单一模块,而是由多个协作组件构成的流水线:
text
网络接收 → 字节流 → 解码器 → 字符流 → 词法分析器 → 标签流 → 语法分析器 → DOM树
↑ ↑
编码管理 脚本执行/自定义元素
4.2 词法分析器(Tokenizer)的有限状态机
HTML词法分析器是一个复杂的状态机,在标准HTML5 Tokenization规范中定义了80+个状态。核心状态包括:
text
初始状态 ("Data state")
↓ 遇到 < 字符
标签开始状态 ("Tag open state")
↓ 遇到 /
结束标签开始 ("End tag open state")
↓ 遇到 字母
标签名状态 ("Tag name state")
↓ 遇到 空格
属性前状态 ("Before attribute name state")
↓ 遇到 字母
属性名状态 ("Attribute name state")
↓ 遇到 =
属性值前状态 ("Before attribute value state")
↓ 遇到 "
属性值双引号状态 ("Attribute value (double-quoted) state")
↓ 遇到 "
标签后状态 ("After attribute value state")
↓ 遇到 >
数据状态 ("Data state")
... (自闭合标签、注释、CDATA等特殊处理)
解析meta标签时的状态转换:
当词法分析器遇到<meta时:
-
Data state → Tag open state (遇到'<')
-
Tag open state → Tag name state (遇到'm')
-
积累标签名"meta"
-
遇到空格 → Before attribute name state
-
遇到'c' → Attribute name state
-
积累属性名"charset"
-
遇到'=' → Before attribute value state
-
遇到'"' → Attribute value (double-quoted) state
-
积累属性值"utf-8"
-
遇到'"' → After attribute value state
-
遇到'>' → Data state(标签结束)
4.3 语法分析器(Tree Builder)的插入模式
词法分析器产生的每个标签(StartTag token、EndTag token、Character token等)会传递给Tree Builder,后者负责维护DOM树结构,并管理"插入模式"(insertion mode)。
插入模式决定了新创建的节点应该被插入到DOM树的哪个位置:
text
初始化 → "Initial" mode
↓ 遇到doctype
"Before html" mode
↓ 遇到<html>
"Before head" mode
↓ 遇到<head>
"In head" mode ← 解析meta标签的主要模式
↓ 遇到<body>或非head内容
"After head" mode
↓ 遇到<body>
"In body" mode
...
meta标签在"In head"模式下的处理:
-
创建HTMLMetaElement对象
-
调用
HTMLMetaElement::ParseAttribute()处理charset、http-equiv等属性 -
如果是charset meta,触发编码切换逻辑
-
将meta节点附加到当前head指针下
4.4 解析器的阻塞机制
脚本阻塞:
-
<script src="...">默认会阻塞解析器,直到脚本加载并执行完毕 -
async和defer属性改变这一行为 -
meta标签不阻塞,但它触发的编码重解析会间接阻塞
样式阻塞:
-
样式表加载不会阻塞HTML解析(DOM构建)
-
但会阻塞脚本执行(因为脚本可能查询样式)
-
CSSOM未构建完成时,js执行会被延迟
4.5 预加载扫描器(Preload Scanner)的工作原理
为了提高性能,浏览器实现了预加载扫描器,它是一个独立的、轻量级的解析器,会快速扫描HTML(甚至在主解析器处理之前),识别出需要加载的资源URL(图片、脚本、样式表等)。
预加载扫描器如何受头部影响:
-
charset必须确定:预加载扫描器需要知道正确的编码,否则会提取错误的URL
-
Base meta的影响:
<base href="...">会改变所有相对URL的解析结果 -
CSP meta的影响:预加载扫描器在遇到CSP meta之前可能已发出请求,这些请求不受CSP限制(安全隐患,因此CSP推荐使用HTTP头而非meta)
第五章:渲染管线的启动与协调
5.1 关键渲染路径的触发时机
渲染管线并非等到整个HTML解析完成才启动,而是采用渐进式渲染:
DOM构建的阶段性触发:
-
解析器每处理一定量的token(通常是“一次宏任务”),会主动让出控制权,允许渲染线程执行一次“机会性渲染”
-
document.readystate的变化会触发事件:-
loading:仍在加载 -
interactive:DOM解析完成(但可能还在加载子资源) -
complete:所有资源加载完成
-
样式计算的启动条件:
-
必须有可用的CSSOM(整个
<style>和<link>的样式表都加载并解析完毕) -
有可用的DOM节点(至少是部分DOM树)
5.2 头部标签如何影响首次渲染
正确的charset:确保文本正确显示,避免FOUC(Flash of Unstyled Content)或乱码闪烁
Viewport设置:移动设备上,首次布局计算时就需要viewport信息。如果meta viewport出现较晚,可能发生:
-
初始布局使用默认viewport(通常是980px)
-
解析到meta viewport后重新布局(导致页面缩放变化,视觉闪烁)
CSS阻塞:<link rel="stylesheet">会阻塞首次渲染(浏览器等待CSSOM构建完成),这是为了避免无样式内容闪烁(FOUC)。但位于body底部的CSS不会阻塞首次渲染。
5.3 布局计算的约束求解过程
布局引擎(如Blink的LayoutNG或Gecko的Servo)在计算盒模型位置和尺寸时,会受doctype影响:
标准模式下的块级元素布局:
-
使用包含块的概念
-
外边距折叠规则严格
-
绝对定位相对于最近的定位祖先
怪异模式下的差异:
-
IE盒模型的宽高包含border/padding
-
行内元素的高度计算不同
-
表格元素有特殊的行高继承逻辑
第六章:实战案例分析
6.1 没有doctype的后果
html
<html>
<head><title>No doctype page</title></head>
<body>
<div style="width: 100px; padding: 10px; background: red;">
Test
</div>
</body>
</html>
-
怪异模式激活
-
盒子实际宽度=100px(包含padding),内容区宽度=80px
-
内联元素的height设置生效
-
基准字体大小、表格布局等表现异常
6.2 charset标签位置的影响
正确位置(前512字节内):
html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>正确示例</title> </head>
错误位置(body内):
html
<!DOCTYPE html> <html> <head><title>乱码风险</title></head> <body> <p>中文内容</p> <meta charset="utf-8"> <!-- 太晚了! --> </body> </html>
后果:解析器在解码中文内容时使用的是默认编码(可能是Windows-1252),产生乱码,然后即使发现了正确的charset,重新解析带来的性能损耗也可能导致渲染中断和闪烁。
6.3 动态插入meta标签的影响
javascript
// 动态创建meta viewport
var meta = document.createElement('meta');
meta.name = 'viewport';
meta.content = 'width=device-width, initial-scale=1.0';
document.head.appendChild(meta);
-
在移动端,此操作会触发页面重新布局
-
可能导致页面的缩放状态突变
-
部分浏览器支持,但规格并不保证动态viewport生效
第七章:性能优化与最佳实践
7.1 关键头部配置模板
html
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"> <meta name="referrer" content="strict-origin-when-cross-origin"> <link rel="preconnect" href="https://fonts.googleapis.com"> <title>页面标题</title> <!-- 内联关键CSS --> <style>/* Critical CSS */</style> <!-- 异步非关键CSS --> <link rel="preload" href="non-critical.css" as="style" οnlοad="this.οnlοad=null;this.rel='stylesheet'"> </head> <body>
7.2 避免的常见陷阱
-
doctype声明前有注释或空白 → 触发怪异模式
html
<!-- 错误:这里有注释 --> <!DOCTYPE html>
-
charset声明太晚 → 额外解码开销 + 乱码风险
-
多个X-UA-Compatible冲突 → 使用第一个meta的内容
-
viewport设置user-scalable=no → 违反可访问性原则
7.3 调试工具的使用
Chrome DevTools:
-
Network面板:查看HTTP头中的Content-Type
-
Console:
document.compatMode查看渲染模式 -
Elements面板:检查实际解析的meta标签
-
Performance面板:观察编码切换导致的重新解析事件
第八章:未来演进与标准化趋势
8.1 新提案对头部解析的影响
Delay header parsing:允许延迟解析特定资源,提高首屏性能
Speculation Rules API:通过meta标签声明预加载策略
html
<meta name="speculation-rules" content="prerender.json">
8.2 模块化HTML解析
新的解析器设计趋势(如Lexical的模块化HTML解析器)将词法分析和语法分析解耦,支持更灵活的流控。
8.3 替代性编码处理
随着UTF-8成为事实标准,未来浏览器可能简化编码嗅探逻辑,优先使用UTF-8,减少复杂的编码切换逻辑。
结语
从doctype到charset,从meta标签到解析器状态机,浏览器头部的处理是一个融合了历史兼容性、性能优化、国际化和安全策略的复杂系统。理解这些底层机制,不仅能帮助我们编写更正确、更高效的HTML,也能在性能调优和问题排查时直击要害。
当我们写出<!DOCTYPE html>时,我们不仅仅是在声明一个文档类型,而是在调用一套经过三十多年演进、数千万行代码构建的全球化信息呈现系统。这个系统的每一个状态转移、每一次模式切换,都凝聚着无数工程师的智慧结晶。
HTML头部虽然仅占文档的极少部分,但它如同一个精密的仪表盘,指导着整个浏览器的运作方式。掌握这些元指令的底层逻辑,是每个深度Web开发者必备的核心能力。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)