+999 votes
,post bởi

Facebook.com ra mắt vào năm 2004 dưới dạng một trang web PHP đơn giản, được kết xuất bởi máy chủ. Theo thời gian, chúng tôi đã thêm từng lớp công nghệ mới để cung cấp nhiều tính năng tương tác hơn. Mỗi tính năng và công nghệ mới này dần dần làm chậm trang web và khiến nó khó bảo trì hơn. Điều này khiến việc giới thiệu trải nghiệm mới trở nên khó khăn hơn. Các tính năng như chế độ tối và lưu vị trí của bạn trong Bảng tin không được triển khai kỹ thuật đơn giản. Chúng tôi cần phải lùi lại một bước để suy nghĩ lại về kiến ​​trúc của mình.

Khi chúng tôi nghĩ về cách chúng tôi sẽ xây dựng một ứng dụng web mới - một ứng dụng được thiết kế cho các trình duyệt ngày nay, với các tính năng mà mọi người mong đợi từ Facebook - chúng tôi nhận ra rằng ngăn xếp công nghệ hiện tại của chúng tôi không thể hỗ trợ cảm giác và hiệu suất giống như ứng dụng mà chúng tôi cần. Việc viết lại hoàn chỉnh là cực kỳ hiếm, nhưng trong trường hợp này, vì đã có quá nhiều thay đổi trên web trong suốt thập kỷ qua, chúng tôi biết đó là cách duy nhất để có thể đạt được mục tiêu về hiệu suất và tăng trưởng bền vững trong tương lai . Hôm nay, chúng tôi chia sẻ những bài học chúng tôi đã học được khi cấu trúc lại Facebook.com, sử dụng React (một thư viện JavaScript khai báo để xây dựng giao diện người dùng) và Relay (một ứng dụng khách GraphQL cho React).

Bắt đầu

Chúng tôi biết rằng chúng tôi muốn Facebook.com khởi động nhanh, phản hồi nhanh và cung cấp trải nghiệm tương tác cao. Mặc dù ứng dụng hướng máy chủ có thể mang lại thời gian khởi động nhanh, nhưng chúng tôi không tin rằng mình có thể làm cho nó tương tác và thú vị như một ứng dụng hướng đến khách hàng. Tuy nhiên, chúng tôi tin rằng chúng tôi có thể xây dựng một ứng dụng hướng đến khách hàng với thời gian khởi động nhanh cạnh tranh.

Nhưng bắt đầu từ đầu với ứng dụng ưu tiên khách hàng đặt ra một loạt vấn đề mới. Chúng tôi cần nhanh chóng xây dựng lại hệ thống công nghệ đồng thời giải quyết các vấn đề về tốc độ và trải nghiệm người dùng khác - và chúng tôi cần làm theo cách để nó có thể bền vững trong nhiều năm tới.

Trong suốt quá trình này, chúng tôi cố định công việc của mình xung quanh hai câu thần chú kỹ thuật:

  1. Càng ít càng tốt, càng sớm càng tốt. Chúng ta chỉ nên cung cấp những nguồn lực chúng ta cần và chúng ta nên cố gắng đưa chúng đến ngay trước khi chúng ta cần.
  2. Kinh nghiệm kỹ thuật phục vụ trải nghiệm người dùng. Mục tiêu cuối cùng của sự phát triển của chúng tôi là tất cả về những người sử dụng trang web của chúng tôi. Khi chúng tôi nghĩ về những thách thức UX trên trang web của mình, chúng tôi có thể điều chỉnh trải nghiệm để hướng dẫn các kỹ sư làm đúng theo mặc định.

Chúng tôi đã áp dụng những nguyên tắc tương tự này để cải thiện bốn yếu tố chính của trang web: CSS, JavaScript, dữ liệu và điều hướng.

Suy nghĩ lại về CSS để mở khóa các khả năng mới

Đầu tiên, chúng tôi đã giảm 80% CSS trên trang chủ bằng cách thay đổi cách chúng tôi viết và xây dựng phong cách của mình. Trên trang web mới, CSS chúng tôi viết khác với những gì được gửi đến trình duyệt. Trong khi chúng tôi viết JavaScript giống CSS quen thuộc trong các tệp giống như các thành phần của chúng tôi, một công cụ xây dựng sẽ chia các kiểu này thành các gói riêng biệt, được tối ưu hóa. Do đó, trang web mới cung cấp ít CSS hơn, hỗ trợ chế độ tối và kích thước phông chữ động cho khả năng truy cập và đã cải thiện hiệu suất hiển thị hình ảnh - tất cả đồng thời giúp các kỹ sư làm việc dễ dàng hơn.

Tạo CSS nguyên tử để giảm 80% CSS trang chủ

Trên trang web cũ của chúng tôi, chúng tôi đã tải hơn 400 KB CSS nén (2 MB không nén) khi tải trang chủ, nhưng chỉ 10% trong số đó thực sự được sử dụng cho lần hiển thị ban đầu. Chúng tôi không bắt đầu với nhiều CSS như vậy; nó chỉ phát triển theo thời gian và hiếm khi giảm. Điều này xảy ra một phần vì mỗi tính năng mới có nghĩa là thêm CSS mới. 

Chúng tôi đã giải quyết vấn đề này bằng cách tạo CSS nguyên tử tại thời điểm xây dựng. Atomic CSS có đường cong tăng trưởng logarit vì nó tỷ lệ với số lượng khai báo kiểu duy nhất hơn là với số kiểu và tính năng chúng tôi viết. Điều này cho phép chúng tôi kết hợp CSS nguyên tử được tạo từ khắp trang web của chúng tôi thành một biểu định kiểu duy nhất, nhỏ, được chia sẻ. Do đó, trang chủ mới tải xuống ít hơn 20 phần trăm CSS mà trang web cũ đã tải xuống.

Định vị kiểu để giảm bớt CSS không sử dụng và giúp bảo trì dễ dàng hơn

Một lý do khác khiến CSS của chúng tôi phát triển theo thời gian là rất khó xác định liệu các quy tắc CSS khác nhau có còn được sử dụng hay không. Atomic CSS giúp giảm thiểu tác động hiệu suất của điều này, nhưng các kiểu duy nhất vẫn thêm các byte không cần thiết và CSS không được sử dụng trong mã nguồn của chúng tôi sẽ bổ sung thêm chi phí kỹ thuật. Bây giờ, chúng tôi định vị các kiểu của mình với các thành phần của chúng tôi để chúng có thể được xóa song song và chỉ chia chúng thành các gói riêng biệt tại thời điểm xây dựng.

Chúng tôi cũng giải quyết một vấn đề khác mà chúng tôi đang gặp phải: Mức độ ưu tiên của CSS phụ thuộc vào việc đặt hàng, điều này đặc biệt khó quản lý khi sử dụng tính năng đóng gói tự động có thể thay đổi theo thời gian. Trước đây, những thay đổi trong một tệp này có thể phá vỡ các kiểu trong tệp khác mà tác giả không nhận ra. Thay vào đó, chúng tôi hiện tạo các kiểu bằng cách sử dụng cú pháp quen thuộc lấy cảm hứng từ các API tạo kiểu React Native : Chúng tôi đảm bảo rằng các kiểu được áp dụng theo thứ tự ổn định và chúng tôi không hỗ trợ các bộ chọn con cháu CSS.

Thay đổi kích thước phông chữ để có khả năng tiếp cận tốt hơn

Chúng tôi cũng đã tận dụng bước xây dựng ngoại tuyến của mình để thực hiện các bản cập nhật trợ năng. Trên nhiều trang web ngày nay, mọi người phóng to văn bản bằng cách sử dụng chức năng thu phóng của trình duyệt. Điều này có thể vô tình kích hoạt bố cục máy tính bảng hoặc thiết bị di động hoặc làm tăng kích thước của những thứ mà chúng không cần phải phóng to, chẳng hạn như hình ảnh.

Bằng cách sử dụng rems , chúng tôi có thể tôn trọng các giá trị mặc định do người dùng chỉ định và có thể cung cấp các điều khiển để tùy chỉnh kích thước phông chữ mà không yêu cầu thay đổi biểu định kiểu. Tuy nhiên, các thiết kế thường được tạo bằng các giá trị pixel CSS. Việc chuyển đổi thủ công sang rems sẽ bổ sung thêm chi phí kỹ thuật và khả năng có lỗi, vì vậy chúng tôi có công cụ xây dựng của chúng tôi thực hiện việc chuyển đổi này cho chúng tôi.

4 Answers

+349 votes
,post bởi (4.4k điểm)

Xử lý thời gian xây dựng mẫu

const styles = stylex.create({
  emphasis: {
    fontWeight: 'bold',
  },
  text: {
    fontSize: '16px',
    fontWeight: 'normal',
  },
});
 
function MyComponent(props) {
  return <span className={styles('text', props.isEmphasized && 'emphasis')} />;
}

Ví dụ về mã nguồn.

.c0 { font-weight: bold; }
.c1 { font-weight: normal; }
.c2 { font-size: 0.9rem; }

Ví dụ về CSS đã tạo.

function MyComponent(props) {
  return <span className={(props.isEmphasized ? 'c0 ' : 'c1 ') + 'c2 '} />;
}

Ví dụ về JavaScript đã tạo.

Các biến CSS cho chủ đề (chế độ tối)

Trên trang web cũ, chúng tôi đã từng cố gắng áp dụng các chủ đề bằng cách thêm tên lớp vào phần tử body và sau đó sử dụng tên lớp đó để ghi đè các kiểu hiện có bằng các quy tắc có tính cụ thể cao hơn. Cách tiếp cận này có vấn đề và nó không còn hoạt động với cách tiếp cận CSS-in-JavaScript nguyên tử mới của chúng tôi, vì vậy chúng tôi đã chuyển sang các biến CSS cho chủ đề.

Các biến CSS được định nghĩa trong một lớp và khi lớp đó được áp dụng cho một phần tử DOM, các giá trị của nó sẽ được áp dụng cho các kiểu trong cây con DOM của nó. Điều này cho phép chúng tôi kết hợp các chủ đề thành một biểu định kiểu duy nhất, có nghĩa là việc chuyển đổi các chủ đề khác nhau không yêu cầu tải lại trang, các trang khác nhau có thể có các chủ đề khác nhau mà không cần tải xuống CSS bổ sung và các sản phẩm khác nhau có thể sử dụng các chủ đề khác nhau song song trên cùng một trang.

.light-theme {
  --card-bg: #eee;
}
.dark-theme {
  --card-bg: #111;
}
.card {
  background-color: var(--card-bg);
}

Điều này làm cho tác động hiệu suất của một chủ đề tỷ lệ thuận với kích thước của bảng màu hơn là kích thước hoặc độ phức tạp của thư viện thành phần. Một gói CSS nguyên tử duy nhất cũng bao gồm triển khai chế độ tối.

SVG trong JavaScript cho hiệu suất hiển thị một lần, nhanh chóng

Để tránh nhấp nháy khi các biểu tượng xuất hiện sau phần còn lại của nội dung, chúng tôi nội tuyến SVG vào HTML bằng cách sử dụng React thay vì chuyển tệp SVG vào thẻ <img> . Bởi vì các SVG này hiện là JavaScript hiệu quả, chúng có thể được đóng gói và phân phối cùng với các thành phần xung quanh của chúng để tạo ra một lần hiển thị rõ ràng. Chúng tôi nhận thấy rằng lợi ích của việc tải các tệp này cùng lúc với JavaScript lớn hơn chi phí của hiệu suất vẽ SVG. Bằng cách nội dòng, không có sự nhấp nháy của các biểu tượng bật vào sau đó.

function MyIcon(props) {
  return (
    <svg {...props} className={styles({/*...*/})}>
       <path d="M17.5 ... 25.479Z" />
    </svg>
  );
}

Ngoài ra, các biểu tượng này có thể thay đổi màu sắc mượt mà trong thời gian chạy mà không yêu cầu tải xuống thêm. Chúng tôi có thể tạo kiểu cho biểu tượng theo các đạo cụ của nó và sử dụng các biến CSS của chúng tôi để tạo chủ đề cho các loại biểu tượng nhất định, đặc biệt là những biểu tượng đơn sắc.

Thiết kế Facebook.com mới ở chế độ ánh sáng: Xây dựng lại ngăn xếp công nghệ của chúng tôi cho Facebook.com mới

Thiết kế lại Facebook.com ở chế độ tối

JavaScript phân tách mã để có hiệu suất nhanh hơn

Kích thước mã là một trong những mối quan tâm lớn nhất với ứng dụng một trang dựa trên JavaScript vì nó có ảnh hưởng lớn đến hiệu suất tải trang. Chúng tôi biết rằng nếu chúng tôi muốn có một ứng dụng React phía máy khách cho Facebook.com, chúng tôi cần giải quyết vấn đề này. Chúng tôi đã giới thiệu một số API mới hoạt động phù hợp với câu thần chú “càng ít càng tốt, càng sớm càng tốt” của chúng tôi.

+620 votes
,post bởi (4.4k điểm)

Tải xuống mã tăng dần để chỉ cung cấp những gì chúng ta cần, khi chúng ta cần

Khi ai đó đang đợi tải trang, mục tiêu của chúng tôi là đưa ra phản hồi ngay lập tức bằng cách hiển thị “bộ xương” giao diện người dùng về trang sẽ trông như thế nào. Bộ xương này cần tài nguyên tối thiểu, nhưng chúng tôi không thể hiển thị nó sớm nếu mã của chúng tôi được đóng gói trong một gói duy nhất, vì vậy chúng tôi cần chia mã thành các nhóm dựa trên thứ tự mà trang sẽ được hiển thị. Tuy nhiên, nếu chúng ta thực hiện điều này một cách ngây thơ (tức là bằng cách sử dụng các phép nhập động được tìm nạp trong quá trình kết xuất), chúng ta có thể làm tổn hại đến hiệu suất thay vì giúp đỡ nó. Đây là cơ sở của thiết kế phân tách mã của chúng tôi về Các lớp tải JavaScript: Chúng tôi chia JavaScript cần thiết cho lần tải ban đầu thành ba lớp, sử dụng API có thể phân tích tĩnh, khai báo.

Bậc 1 là bố cục cơ bản cần thiết để hiển thị lớp sơn đầu tiên cho nội dung trong màn hình đầu tiên, bao gồm các khung giao diện người dùng cho các trạng thái tải ban đầu.

Bậc 1 là bố cục cơ bản cần thiết để hiển thị lớp sơn đầu tiên cho nội dung trong màn hình đầu tiên, bao gồm các khung giao diện người dùng cho các trạng thái tải ban đầu.

Trang sau khi mã Cấp 1 tải và hiển thị.

import ModuleA from 'ModuleA';

Bậc 1 sử dụng importcú pháp thông thường.

Cấp 2 bao gồm tất cả JavaScript cần thiết để hiển thị đầy đủ tất cả nội dung trong màn hình đầu tiên. Sau Cấp 2, không có gì trên màn hình sẽ vẫn thay đổi trực quan do quá trình tải mã.

Cấp 2 bao gồm tất cả JavaScript cần thiết để hiển thị đầy đủ tất cả nội dung trong màn hình đầu tiên.  Sau Cấp 2, không có gì trên màn hình sẽ vẫn thay đổi trực quan do quá trình tải mã.

Trang sau khi mã Cấp 2 tải và hiển thị.

importForDisplay ModuleBDeferred from 'ModuleB';

Sau khi importForDisplaygặp phải, nó và các phụ thuộc của nó được chuyển vào Cấp 2. Điều này trả về một trình bao bọc dựa trên lời hứa để truy cập vào mô-đun sau khi nó được tải.

Cấp 3 bao gồm mọi thứ chỉ cần thiết sau khi hiển thị mà không ảnh hưởng đến các pixel hiện tại trên màn hình, bao gồm mã ghi nhật ký và đăng ký cho dữ liệu cập nhật trực tiếp.

Bậc 2 cần phải tương tác đầy đủ. Nếu ai đó nhấp vào menu sau khi mã Cấp 2 tải và hiển thị, họ sẽ nhận được phản hồi ngay lập tức về tương tác, ngay cả khi nội dung của menu chưa sẵn sàng hiển thị.

Cấp 3 bao gồm mọi thứ chỉ cần thiết sau khi hiển thị mà không ảnh hưởng đến các pixel hiện tại trên màn hình, bao gồm mã ghi nhật ký và đăng ký cho dữ liệu cập nhật trực tiếp.

importForAfterDisplay ModuleCDeferred from 'ModuleC';
 
// ...
 
function onClick(e) {
  ModuleCDeferred.onReady(ModuleC => {
    ModuleC.log('Click happened! ', e);
  });
}

Sau khi importForAfterDisplaygặp phải, nó và các phụ thuộc của nó được chuyển vào Cấp 3. Điều này trả về một trình bao bọc dựa trên lời hứa để truy cập vào mô-đun sau khi nó được tải.

Một trang JavaScript 500 KB có thể trở thành 50 KB ở Bậc 1, 150 KB ở Bậc 2 và 300 KB ở Bậc 3. Việc chia nhỏ mã của chúng tôi theo cách này cho phép chúng tôi cải thiện thời gian vẽ lần đầu và thời gian hoàn thành trực quan bằng cách giảm số lượng mã cần được tải xuống để đạt được từng cột mốc. Bởi vì Tier 3 không ảnh hưởng đến các pixel trên màn hình, nó thực sự không phải là bản kết xuất và lớp sơn cuối cùng sẽ hoàn thành sớm hơn. Đáng kể nhất, màn hình tải có thể hiển thị sớm hơn nhiều.

Chỉ cung cấp các phần phụ thuộc theo hướng thử nghiệm khi chúng cần

Chúng tôi thường cần hiển thị hai biến thể của cùng một giao diện người dùng, ví dụ: trong thử nghiệm A / B. Cách đơn giản nhất để làm điều này là tải xuống cả hai phiên bản cho tất cả mọi người, nhưng điều này có nghĩa là chúng tôi thường tải xuống mã không bao giờ được thực thi. Một cách tiếp cận tốt hơn một chút là sử dụng nhập động khi kết xuất, nhưng điều này có thể chậm.

Thay vào đó, để phù hợp với câu thần chú “càng ít càng tốt, càng sớm càng tốt”, chúng tôi đã xây dựng một API khai báo để cảnh báo chúng tôi về những quyết định này sớm và mã hóa chúng trong biểu đồ phụ thuộc của chúng tôi. Khi trang đang tải, máy chủ có thể kiểm tra thử nghiệm và chỉ gửi phiên bản mã bắt buộc.

const Composer = importCond('NewComposerExperiment', {
  true: 'NewComposer',
  false: 'OldComposer',
});

Điều này hoạt động tốt khi các điều kiện mà chúng tôi phân chia là tĩnh trên các lần tải trang cho người đó, chẳng hạn như kiểm tra A / B, ngôn ngữ hoặc lớp thiết bị.

+761 votes
,post bởi (4.4k điểm)

Bản đồ và định nghĩa tuyến đường để điều hướng nhanh hơn

Điều hướng nhanh là một tính năng quan trọng của các ứng dụng một trang. Khi điều hướng đến một tuyến đường mới, chúng tôi cần tìm nạp nhiều mã và dữ liệu khác nhau từ máy chủ để hiển thị trang đích. Để giảm số lượng các chuyến đi vòng quanh mạng cần thiết khi tải một trang mới, khách hàng cần biết trước thời hạn tài nguyên nào sẽ cần thiết cho mỗi tuyến. Chúng tôi gọi đây là bản đồ tuyến đường và mỗi mục nhập là một định nghĩa tuyến đường.

Nhận định nghĩa tuyến đường càng sớm càng tốt

Đối với Facebook, bản đồ lộ trình này quá lớn để gửi tất cả cùng một lúc. Thay vào đó, chúng tôi tự động thêm các định nghĩa tuyến đường vào bản đồ tuyến đường trong phiên, khi các liên kết mới được hiển thị. Bản đồ tuyến đường và bộ định tuyến nằm ở trên cùng của ứng dụng, cho phép sự kết hợp giữa ứng dụng hiện tại và trạng thái bộ định tuyến để thúc đẩy các quyết định về trạng thái cấp ứng dụng, chẳng hạn như hoạt động của thanh điều hướng trên cùng hoặc các tab trò chuyện dựa trên tuyến đường hiện tại.

Tìm nạp trước các tài nguyên càng sớm càng tốt

Các ứng dụng phía máy khách thường đợi cho đến khi một trang được React hiển thị để tải xuống mã và dữ liệu cần thiết cho trang đó. Thường thì điều này được thực hiện bằng cách sử dụng React.lazy hoặc một phần mềm nguyên thủy tương tự. Vì điều này có thể làm cho việc điều hướng trang chậm lại, thay vào đó, chúng tôi bắt đầu yêu cầu đầu tiên đối với một số tài nguyên cần thiết ngay cả trước khi một liên kết được nhấp vào:

Chúng tôi bắt đầu tìm nạp sớm, tải trước khi di chuột hoặc lấy tiêu điểm và tìm nạp khi hạ xuống.  Ví dụ này dành riêng cho máy tính để bàn, nhưng các phương pháp phỏng đoán khác có thể được sử dụng cho các thiết bị cảm ứng.

Chúng tôi bắt đầu tìm nạp sớm, tải trước khi di chuột hoặc lấy tiêu điểm và tìm nạp khi hạ xuống. Ví dụ này dành riêng cho máy tính để bàn, nhưng các phương pháp phỏng đoán khác có thể được sử dụng cho các thiết bị cảm ứng.

Để mang lại trải nghiệm linh hoạt hơn thay vì chỉ hiển thị màn hình trống khi điều hướng, chúng tôi sử dụng chuyển tiếp React Suspense để tiếp tục hiển thị tuyến trước đó cho đến khi tuyến tiếp theo được hiển thị đầy đủ hoặc tạm dừng ở trạng thái tải “tốt” với bộ khung giao diện người dùng cho trang tiếp theo . Điều này ít chói tai hơn nhiều và nó bắt chước hành vi tiêu chuẩn của trình duyệt.

Song song mã và tải xuống dữ liệu

Chúng tôi thực hiện rất nhiều thao tác tải mã chậm trên trang web mới, nhưng nếu chúng tôi lười tải mã cho một tuyến và mã tìm nạp dữ liệu cho tuyến đó nằm bên trong mã đó, chúng tôi sẽ kết thúc với một lần tải nối tiếp.

Một ứng dụng React / Relay “truyền thống” với các tuyến đường được tải chậm dẫn đến hai chuyến đi khứ hồi.

Một ứng dụng React / Relay “truyền thống” với các tuyến đường được tải chậm dẫn đến hai chuyến đi khứ hồi.

Để giải quyết vấn đề này, chúng tôi đã đưa ra EntryPoints, là các tệp bao bọc một điểm phân tách mã và chuyển đổi đầu vào thành truy vấn. Các tệp này rất nhỏ và được tải xuống trước cho bất kỳ điểm phân tách mã nào có thể truy cập được.

Mã và dữ liệu được tìm nạp song song, cho phép chúng tôi tải xuống những thứ này trong một vòng mạng duy nhất.

Mã và dữ liệu được tìm nạp song song, cho phép chúng tôi tải xuống những thứ này trong một vòng mạng duy nhất.

Truy vấn GraphQL vẫn được định vị với dạng xem, nhưng EntryPoint sẽ đóng gói khi cần truy vấn đó và cách chuyển đổi các đầu vào thành các biến chính xác. Ứng dụng sử dụng các EntryPoints này để tự động quyết định thời điểm tìm nạp tài nguyên, đảm bảo điều phù hợp xảy ra theo mặc định. Điều này có lợi ích bổ sung là tạo một hàm JavaScript duy nhất chứa tất cả các nhu cầu tìm nạp dữ liệu cho bất kỳ điểm nhất định nào trong ứng dụng, có thể được sử dụng để tải trước máy chủ đã thảo luận trước đó.

Nhiều thay đổi mà chúng tôi đã thảo luận ở đây không dành riêng cho Facebook. Những khái niệm và mẫu này có thể được áp dụng cho bất kỳ ứng dụng phía máy khách nào bằng cách sử dụng bất kỳ khung hoặc thư viện nào. Bằng cách tiêu chuẩn hóa ngăn xếp công nghệ của mình, chúng tôi đã có thể suy nghĩ lại cách chúng tôi giới thiệu chức năng mà mọi người muốn một cách hiệu quả, bền vững - ngay cả khi chúng tôi hoạt động ở quy mô sản phẩm và kỹ thuật. 

Cải tiến trải nghiệm kỹ thuật và cải tiến trải nghiệm người dùng phải đi đôi với nhau và không thể xem hiệu suất và khả năng tiếp cận như một loại thuế đánh vào các tính năng vận chuyển. Với các API, công cụ và tự động hóa tuyệt vời, chúng tôi có thể giúp các kỹ sư di chuyển nhanh hơn và gửi mã tốt hơn, hiệu quả hơn cùng một lúc. Công việc được thực hiện để cải thiện hiệu suất cho Facebook.com mới là rất nhiều và chúng tôi hy vọng sẽ sớm chia sẻ thêm về công việc này. Để kiểm tra thiết kế lại, hãy truy cập facebook.com/new . Nó sẽ ra mắt dần dần và sẽ sớm có sẵn cho mọi người.

+151 votes
,post bởi (440 điểm)

Chỉ cung cấp các phần phụ thuộc theo hướng dữ liệu khi chúng cần

Điều gì về các nhánh mã không tĩnh qua các lần tải trang? Ví dụ: gửi xuống tất cả mã hiển thị cho tất cả các loại và kết hợp thành phần khác nhau cho các bài đăng trên News Feed sẽ làm tăng kích thước JavaScript của trang lên đáng kể.

Những phụ thuộc này được quyết định trong thời gian chạy, dựa trên dữ liệu đó được trả về từ back end. Điều này cho phép chúng tôi sử dụng một tính năng mới của Relay để thể hiện mã kết xuất nào là cần thiết, tùy thuộc vào loại dữ liệu được trả về. Nếu bài đăng có tệp đính kèm đặc biệt, chẳng hạn như ảnh, chúng tôi mô tả rằng chúng tôi cần PhotoComponent để hiển thị ảnh đó.

... on Post {
  ... on PhotoPost {
    @module('PhotoComponent.js')
    photo_data
  }
  ... on VideoPost {
    @module('VideoComponent.js')
    video_data
  }
}

Chúng tôi thể hiện các phụ thuộc cần thiết để hiển thị từng loại bài đăng như một phần của truy vấn.

Thậm chí tốt hơn, PhotoComponentbản thân nó mô tả chính xác dữ liệu nào trên loại tệp đính kèm ảnh mà nó cần dưới dạng một đoạn, có nghĩa là chúng ta thậm chí có thể tách ra logic truy vấn.

Sử dụng ngân sách JavaScript để ngăn chặn lỗi mã

Các lớp và phần phụ thuộc có điều kiện giúp chúng tôi chỉ cung cấp mã cần thiết cho mỗi giai đoạn, nhưng chúng tôi cũng cần đảm bảo kích thước của mỗi lớp luôn được kiểm soát theo thời gian. Để quản lý điều này, chúng tôi đã giới thiệu ngân sách JavaScript cho mỗi sản phẩm.

Chúng tôi đặt ngân sách dựa trên mục tiêu hiệu suất, hạn chế kỹ thuật và cân nhắc sản phẩm. Chúng tôi đã phân bổ ngân sách cấp trang và chia nhỏ trang dựa trên ranh giới sản phẩm và ranh giới nhóm. Cơ sở hạ tầng dùng chung được thêm vào danh sách được tuyển chọn cẩn thận và có ngân sách riêng. Cơ sở hạ tầng được chia sẻ tính vào ngân sách của tất cả các trang, nhưng các mô-đun bên trong nó được miễn phí cho các nhóm sản phẩm sử dụng. Chúng tôi cũng có ngân sách cho mã được trả chậm, tải có điều kiện hoặc tải khi tương tác.

Chúng tôi đã tạo công cụ bổ sung cho từng bước của quy trình:

  • Công cụ đồ thị phụ thuộc giúp bạn dễ dàng hiểu byte đến từ đâu và xác định các cơ hội để giảm kích thước mã.
  • Giám sát kích thước trên các yêu cầu hợp nhất hiển thị các cải tiến / hồi quy kích thước và kích hoạt các cảnh báo có thể tùy chỉnh.
  • Biểu đồ tương tác hiển thị kích thước lịch sử và mọi thứ đã thay đổi như thế nào giữa các lần sửa đổi.
  • Trang tổng quan giúp chúng tôi hiểu trạng thái hiện tại của quy mô liên quan đến ngân sách.

Hiện đại hóa việc tìm nạp dữ liệu để lấy nó sớm nhất có thể 

Là một phần của quá trình xây dựng lại này, chúng tôi đã hiện đại hóa cơ sở hạ tầng tìm nạp dữ liệu của mình trên web. Mặc dù một số tính năng của trang web cũ sử dụng Relay và GraphQL để tìm nạp dữ liệu, nhưng hầu hết dữ liệu được tìm nạp đặc biệt như một phần của kết xuất PHP phía máy chủ của chúng. Với trang web mới, chúng tôi đã có thể chuẩn hóa với các ứng dụng dành cho thiết bị di động của mình và đảm bảo rằng tất cả việc tìm nạp dữ liệu đều thông qua GraphQL. Vì Relay và GraphQL đã xử lý công việc “ít nhất có thể” cho chúng tôi, chúng tôi chỉ cần thực hiện một số thay đổi để hỗ trợ nhận được dữ liệu chúng tôi cần sớm nhất có thể.

Tải trước dữ liệu theo yêu cầu máy chủ ban đầu để cải thiện khởi động

Nhiều ứng dụng web cần phải đợi cho đến khi tất cả JavaScript của chúng được tải xuống và thực thi trước khi tìm nạp dữ liệu từ máy chủ. Với Relay, chúng tôi biết trang cần dữ liệu gì. Điều này có nghĩa là ngay sau khi máy chủ của chúng tôi nhận được yêu cầu cho một trang, nó có thể bắt đầu chuẩn bị ngay dữ liệu cần thiết và tải xuống song song với mã được yêu cầu. Chúng tôi truyền trực tuyến dữ liệu này với trang khi nó có sẵn để khách hàng có thể tránh các chuyến đi vòng bổ sung và hiển thị nội dung trang cuối cùng sớm hơn.

Truyền dữ liệu cho ít chuyến đi khứ hồi hơn và tương tác tốt hơn

Trong lần tải đầu tiên của Facebook.com, một số nội dung ban đầu có thể bị ẩn hoặc hiển thị bên ngoài khung nhìn. Ví dụ: hầu hết các màn hình phù hợp với một hoặc hai bài đăng trên News Feed, nhưng chúng tôi không biết trước bao nhiêu màn hình sẽ phù hợp. Ngoài ra, rất có thể người dùng sẽ cuộn và sẽ mất thời gian để tìm nạp từng câu chuyện riêng lẻ trong một chuyến khứ hồi nối tiếp. Mặt khác, chúng tôi tìm nạp càng nhiều câu chuyện trong một truy vấn, thì truy vấn đó càng chậm, dẫn đến thời gian truy vấn lâu hơn và thời gian Toàn bộ trực quan lâu hơn cho ngay cả câu chuyện đầu tiên.

Để giải quyết vấn đề này, chúng tôi sử dụng tiện ích mở rộng GraphQL nội bộ @stream, để truyền trực tuyến kết nối nguồn cấp dữ liệu đến máy khách cho cả lần tải ban đầu và phân trang tiếp theo khi cuộn. Điều này cho phép chúng tôi gửi từng câu chuyện nguồn cấp dữ liệu ngay khi nó sẵn sàng, từng câu chuyện một, chỉ với một thao tác truy vấn duy nhất.

fragment HomepageData on User {
  newsFeed(first: 10) {
    edges @stream
  }
  ...AdditionalData
}

Trì hoãn dữ liệu không cần thiết ngay lập tức

Các phần khác nhau của các truy vấn nhất định mất nhiều thời gian để tính toán hơn các phần khác. Ví dụ: khi xem hồ sơ, việc tìm nạp tên và ảnh hồ sơ của một người tương đối nhanh nhưng sẽ mất nhiều thời gian hơn một chút để tìm nạp nội dung của Dòng thời gian của họ.

Để tìm nạp cả hai loại dữ liệu với một truy vấn duy nhất, chúng tôi sử dụng @defer, điều này cho phép các phần khác nhau của phản hồi được truyền trực tuyến ngay khi chúng sẵn sàng. Điều này cho phép chúng tôi hiển thị phần lớn giao diện người dùng với dữ liệu ban đầu nhanh nhất có thể và hiển thị trạng thái tải cho phần còn lại. Với React Suspense , điều này thậm chí còn dễ dàng hơn, vì chúng tôi có thể tạo các trạng thái tải một cách rõ ràng để đảm bảo trải nghiệm tải trang từ trên xuống mượt mà.

fragment ProfileData on User {
  name
  profile_picture { ... }
  ...AdditionalData @defer
}
...