数据库查询的N+1问题

在工作中有时也会涉及到后端开发,后端开发的性能优化多是查询优化。类似前端 DOM 操作很消耗性能,后端开发数据库查询次数也会相当消耗性能。我们在开发中应该尽量减少 SQL 查询次数,避免 N+1 问题来提升性能。接下来就来介绍数据库查询的 N+1 问题。

# SQL N+1

举一个简单的例子,数据库中有一张 cars 表存储汽车的相关信息,还有一张 wheels 表存储汽车的轮胎信息。

查询汽车的语句是:

SELECT * FROM cars;
1

查询每个汽车的轮子的语句是:

SELECT * FROM wheels WHERE car_id = ${car_id};
1

这里的 N 就是汽车的数量,如果需要知道每个汽车的轮胎信息那就正好需要查询 N+1 次。

# 解决 N+1

如果汽车和轮胎是一对一的关系可以通过使用 join 语句来减少数据库查询次数。把查询降为 1 次。修正之后的查询语句是:

SELECT cars.*, wheels.* FROM cars INNER JOIN wheels ON wheels.car_id = cars.id
1

如果汽车和轮胎是一对多的关系可以通过使用:

SELECT * FROM cars;
SELECT * FROM wheels WHERE wheels.car_id IN (1,2,3)
1
2

一次性查询所有需要的轮胎把查询降为两次。

# 现有框架解决方案

# Django

Django 可以使用select-related (opens new window)prefetch-related (opens new window)来解决这个问题。

select-related 主要用于一对一的关系,而 prefetch-related 主要用于多对多或者多对一的关系。

# Ruby on Rails

使用 includes (opens new window) 来解决 N+1 问题。

# 总结

后端开发需要时刻注意 N+1 问题, 在开发模式下,日志会输出 SQL 查询语句,需要多加注意。也可以使用一些工具来检测这个问题。

# 参考

What is the “N+1 selects problem” in ORM (opens new window)

Active Record Query Interface (opens new window)

提高网页的易读性

正常人可以通过鼠标和键盘来操作浏览器,使用滚轮来滚动网页,使用鼠标左键和右键来执行操作,使用键盘输入内容。但是对于残疾人来说可能就不是这样的了,如果一个人身体行动不便只能通过键盘敲击来操作电脑,无法使用鼠标的情况下,如果网页不支持键盘 Tab 键来切换各种点击按钮,那就会给残疾人的使用带来困难。如果视力不好的人只能借助屏幕阅读器来查看网页,如果按钮或者图片没有合适的标签信息,那就无法通过屏幕阅读器来听到这个按钮或者图片的含义。我们不仅要考虑正常人如何查看网页,也需要顾及到特殊人群的需求,这样才能让每个人都享受到互联网带来的便利。

# focus

focus 状态对于网页易读性非常重要,foucs 决定了键盘事件的去向。但是我们在项目开发的时候经常会给 button 添加鼠标的 hover 状态,却忘记了添加 focus 状态。添加 hover 状态的 CSS 时,记得同时添加 focus 状态的 CSS。同时,浏览器会有默认的 focus 样式, Chrome 下是使用 outline 属性添加高亮的蓝色边框,记得不要轻易覆盖这个属性为 none。 但是并不是所有的 HTML 元素都是可以 focus 的,下面是查找可以 focus 的元素的方法。

const candidateSelectors = [
  'input:not([disabled])',
  'select:not([disabled])',
  'textarea:not([disabled])',
  'a[href]',
  'area[href]',
  'button:not([disabled])',
  '[tabindex="0"]',
  'audio[controls]',
  'video[controls]',
  '[contenteditable]:not([contenteditable="false"])',
  'details>summary'
];
const candidateSelector = candidateSelectors.join(',');

type Option = {
  shouldIgnoreVisibility: boolean;
};
const defaultOption: Option = {
  shouldIgnoreVisibility: false
};

function isDisplayNone(node: Element | null): boolean {
  if (!node) {
    return false;
  }
  if (getComputedStyle(node).display === 'none') return true;
  return isDisplayNone(node.parentElement);
}

function isHidden(node: Element) {
  if (getComputedStyle(node).visibility === 'hidden') return true;
  if (isDisplayNone(node)) return true;

  return false;
}

export function tabbable(el: HTMLElement, option?: Partial<Option>) {
  const mergedOption = {
    ...defaultOption,
    ...option
  };
  const candidates = Array.from(
    el.querySelectorAll<HTMLElement>(candidateSelector)
  );
  if (mergedOption.shouldIgnoreVisibility) {
    return candidates;
  }
  return candidates.filter(candidate => !isHidden(candidate));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

# DOM Order

当你使用键盘的 Tab 来选择下一个按钮的时候,是如何决定顺序的呢? 答案是根据 DOM 的顺序而不是界面上 UI 的顺序,你可以使用 CSS 来调整 UI 顺序,但是 Tab 顺序不会改变。 针对隐藏的元素, Tab 键也不会触发这个元素的 focus,隐藏的元素 CSS 属性可能含有 display: none 或者 visibility: hidden

# tabindex

对于 buttona 元素,当然是可以 focus 的,但是针对类似 div 这样的元素,如何让它可以 focus 呢? 答案是添加 tabindex 属性。

<custom-button tabindex="0">Press Tab to Focus Me!</custom-button>
1

通过添加 tabindex="0" 可以使这个元素被 focus。那么如何使一个元素不被 focus呢?

<button id="foo" tabindex="-1">I'm not keyboard focusable</button>
<button onclick="foo.focus();">Focus my sibling</button>
1
2

使用 tabindex="-1" 可以确保这个元素不会使用键盘 focus,但是鼠标没有影响。 tabindex 当然可以设置大于 0 的值,但我们不推荐这么做,因为默认元素的 tabindex 都是 0,所以大于 0 的元素最后才会触发 focus。 现在的网页开发,我们都会自己手动实现 select 或者 dropdown 元素,但由于这些不是默认的 HTML 元素,所以需要自己实现键盘的 Tab 键或者方向键的触发。

# Modals and keyboard traps

网页开发中经常会出现弹窗,在有弹窗的情况下我们是不能选中背景中的元素的,所以我们需要手动处理 Tab 键保证只有 Modal 里的元素可以被 focus。并且在关闭 Modal 的时候 focus 回原来的元素。

# Semantics and ARIA

我们经常使用 HTML 元素来模拟一些功能,例如使用 button 按钮来模拟菜单功能。这时候我们需要为 button 添加 aria-label 属性方便屏幕阅读器理解这个按钮代表的含义。

<button aria-label="menu"></button>
1

当然啦,这种类似的属性还有很多,例如 role 属性来描述这个元素的定位。

# Accessibility Review

如何检查自己网页的易读性呢,现在也有现成的网页工具和可以集成的 CI 工具。我们可以使用 Chrome 浏览器自带的 Lighthouse 来检查自己网页的易读性。如果想集成 CI 的话,可以试试axe-core (opens new window)

# 总结

提高网页的易读性,不仅可以使用键盘提高操作网页的便利性,也可以照顾特殊人群的使用,何乐而不为呢。

# 参考

Modals and keyboard traps sample code (opens new window)

Accessibility (opens new window)

WAI-ARIA Authoring Practices (opens new window)

JavaScript的加载优化

前端开发中,JavaScript 变得越来越重要,页面载入中,JavaScript 占的比重也更高。不同与别的静态资源,例如图片和 SVG 图标。浏览器在解析 JavaScript 时花的时间要多得多。所以优化 JavaScript 也就变得非常重要。

# 减少 JavaScript 的网络传输开销

# Only sending the code a user needs.

一方面,只加载当前页面需要的 JavaScript 文件,可以使用 webpack 的code-splitting (opens new window)来解决。另一方面,懒加载一些不是必须的 JavaScript 文件。

# Minification

使用terser (opens new window)来压缩 JavaScript 代码。

# Compression

服务器开启 GZIP 来减少传输的文件大小。

# Removing unused code.

针对一些第三方 JavaScript 库,可以使用Tree Shaking (opens new window)来移除不需要的代码。

# Caching code to minimize network trips.

使用 ETag (opens new window), Last-Modified (opens new window), Cache-Control (opens new window) 三个配合有效的缓存 JavaScript 文件。

# 优化 JavaScript 下载和执行

我们可以通过使用 async 或者 defer 来优化 JavaScript 的下载和执行,两个关键字的具体作用看下图:

js async defer

# 总结

JavaScript 的优化并没有银弹,要根据具体项目的实际情况来处理。

# 参考

HTTP caching (opens new window)

JavaScript Start-up Optimization (opens new window)

Loading Third-Party JavaScript (opens new window)