JS – Lựa chọn hiệu năng xử lý performance hay tính dễ đọc readability?

Kinh Nghiệm

JS đang ngày càng phát triển theo thiên hướng là 1 ngôn ngữ lập trình có tính dễ đọc hơn (readable). Tất nhiên việc phát triển theo hướng này là 1 điều tốt.

Phát triển phần mềm là một thị trường năng động, nơi các nhóm phát triển thay đổi liên tục về mặt nhân sự, có nghĩa là mã code cần phải trở nên dễ đọc đối với người mới. Tuy nhiên chi phí về hiệu năng xử lý thì sao?

Khi nào chúng ta cần đưa ra lựa chọn giữa hiệu năng xử lý performance và tính dễ đọc readability? Khi nào chúng ta cần phải lựa chọn 1 cái và hi sinh cái còn lại? Vào thời điểm nào chúng ta cần loại bỏ 1 cái trong số chúng để đưa ra giải pháp quyết định?

Đây là 1 vài câu hỏi mà tôi sẽ trả lời cho bạn, hay ít nhất là chúng ta cùng cố gắng tìm câu trả lời cùng nhau.

Lý do mà chúng ta cố gắng làm đoạn code có hiệu năng xử lý tối ưu là 1 lý do hết sức hiển nhiên, nhưng tại sao chúng ta lại bị ám ảnh bởi tính dễ đọc?

Cùng 1 vấn đề, nhưng cách giải quyết khác nhau

Ok, nhìn lại 1 chút, chắc hẳn ta đã bắt gặp rất quen thuộc các đoạn code dùng để giải quyết vấn đề như sau:

Cho 1 mảng array với các phần tử là số numbers không có thứ tự sắp xếp, cần tạo ra 1 mảng array mới thỏa mãn: được cộng thêm 1 vào mỗi giá trị phần tử và được thực hiện sắp xếp từ bé tới lớn mà không làm thay đổi mảng array gốc.

var numbers = [2, 4, 12, 6, 8, 29, 5, 10, 87, 11, 7];

function process(arr) {
    let newArr = arr.slice(); // tạo ra 1 mảng array mới từ mảng array gốc
    newArr[0]++;
    for (let i = 1; i < newArr.length; i++) {
        const current = newArr[i] + 1;
        let leftIndex = i - 1;

        while (leftIndex >= 0 && newArr[leftIndex] > current) {
            newArr[leftIndex + 1] = newArr[leftIndex];
            leftIndex = leftIndex - 1;
        }
        newArr[leftIndex + 1] = current;
    }
    return newArr;
}

const newArray = process(numbers);

Trong ví dụ trên, tôi sử dụng giải thuật insertion sort, bởi vì nó rất dễ để thực hiện.

Đoạn code ở ví dụ trên không hề dễ đọc chút nào, nhưng nó lại tối ưu về mặt hiệu năng, theo cách làm này thì nó có hiệu năng tốt hơn rất nhiều so với cách giải quyết bằng cách sử dụng code bằng ES6 code có tính dễ đọc hơn:

const process = (arr) => arr
    .map(num => num + 1)
    .sort((a, b) => a - b);

const newArray = process(numbers);

Thực tế thì đoạn code ở ví dụ đầu tiên xử lý nhanh hơn 75% so với đoạn code bên trên, ngay cả đoạn code này dễ đọc hơn và có vẻ đơn giản hơn, khi ta có thể gói gọn nó trong đúng 1 dòng code như sau:

const newArray = numbers.map(num => num + 1).sort((a, b) => a - b);

Hay ta có thể chia nhỏ nó thành các hàm trợ giúp helper function để code dễ để đọc hiểu hơn:

const addOne = (n) => n + 1;
const asc = (a, b) => a - b;
const newArray = numbers.map(addOne).sort(asc);

Với code có tính dễ đọc hiểu, thì chúng ta có thể hướng dẫn các nhà phát triển mới tiếp cận với projects nhanh hơn, có thể chia sẻ đoạn code kiểu vậy dễ dàng hơn và nó cũng trở nên dễ bảo trì hơn.

Tất cả những thứ mà ta đề cập ở trên, thì hiệu năng performance đều không được nhắc đến trong hầu hết các trường hợp. Đây là lí do mà tại sao ES6 lại phát triển theo thiên hướng dễ đọc.

Cuối cùng ta đưa ra sự so sánh giữa 2 cách tiếp cận này:


https://jsperf.com/copy-add-sort-array

Tại đây, bạn dường như sẽ đặt ra câu hỏi cho chính bản thân mình “Ta sẽ nhận được gì khi chấp nhận giảm đi hiệu năng xử lý performant đổi lại tính dễ đọc và dễ hiểu hơn?”

Ok, ta sẽ cùng nhau xem xét 1 vài ví dụ đề cập tới 2 cách tiếp cận này

Spread syntax vs Object.assign()

Ta cùng xem xét ví dụ đơn giản sau:

Thực hiện copy 1 đối tượng object và thêm vào 1 thuộc tính mới vào đối tượng copy

const params = {...}; // filled Object

// ES6 - Spread syntax
var copy1 = { a: 2, ...params };

// Object.assign()
var copy2 = Object.assign({}, { a: 2 }, params);

Cả 2 cách tiếp cận này đều thực hiện cùng 1 công việc, nhưng chúng ta hoàn toàn nhận thấy rằng spread syntax là dễ đọc hơn hẳn mặc dù nó chậm hơn ~54%.


https://jsperf.com/comparing-object-asign-spread/

For loop vs Reduce

Đặt vấn đề:

Tính tổng tất cả giá trị values của 1 mảng array

Giải pháp, xét cách chúng ta thực hiện theo for…loop truyền thống:

const numbers = [2, 4, 12, 6, 8, 29, 5, 10, 87, 11, 7];

function arraySum(arr) {
    let sum = 0;
    for (let i = 0; i < arr.length; i++) {
        sum += arr[i]
    }
    return sum;
}

const sumOfNumbers = arraySum(numbers);

Giờ chúng ta thực hiện bằng reduce:

const numbers = [2, 4, 12, 6, 8, 29, 5, 10, 87, 11, 7];

const add = (a, b) => a + b;
const arraySum = (arr) => arr.reduce(add);

const sumOfNumbers = arraySum(numbers);

Trong ví dụ này, chắc chắn reduce sẽ tốn chi phí về hiệu năng hơn, và nó chậm hơn ví dụ trước tới ~96%


https://jsperf.com/for-loop-vs-reduce-sum

For vs While vs Do while


https://jsperf.com/sraxi-for-vs-while

Khi nào ta nên lựa chọn giải pháp cho phù hợp?

OK, sự thật là tôi đang sử dụng spread syntax, reduce, etc cho tất cả các thao tác xử lý!

Vấn đề nảy sinh tại đây, là theo cách tiếp cập mà tôi chọn thì nó làm cho code trở nên dễ đọc hơn, tuy nhiên nó lại không đi kèm hiệu năng performace.

Vậy “Khi nào ta nên lựa chọn theo giải pháp nào?”

Câu trả lời là rất đơn giản là tùy vào bài toán ta thực hiện.

Quay trở lại ví dụ đầu tiên, nếu chúng ta phải thực hiện copy, add hay sort 1 Array hay Object có kích thước nhỏ hay vừa …Thì chúng ta nên theo thiên hướng dễ đọc, ta sẽ sử dụng toàn bộ code theo ES6.

Một thực tế, là hầu hết tất cả code của ta đều hướng tới tính dễ đọc hơn là hiệu năng.

Khi nào ta nên ưu tiên tính dễ đọc

  • Khi dữ liệu data chúng ta xử lý không quá lớn
  • Khi ứng dụng đang chạy, vẫn chạy đúng với hiệu năng và tải yêu cầu
  • Khi làm việc trong môi trường có nhiều thay đổi với nhiều người mới tiếp cận với dự án
  • Khi viết 1 thư viện library hay plugin cung cấp cho những người khác cần đọc và hiểu

Khi nào ta nên ưu tiên hiệu năng

  • Khi xử lý dữ liệu data lớn
  • Khi ứng dụng chạy chậm và có vấn đề về hiệu năng
  • Khi project cần mở rộng (scale)
  • Khi làm việc trong dự án cá nhân, bạn thích code thế nào thì tùy bạn

Do vậy, nếu chúng ta cần thao tác xử lý với dữ liệu data lớn, thì ta tránh sử dụng reduce, filter, map, spread syntax, etc. Đặc biệt đối với đoạn code mà xử lý liên quan tới Object và Array thì càng không nên dùng.

Kết luận

Thay vì nhảy ngay vào những thứ mới mẻ và cuốn hút, chúng ta nên xem xét lại và phân tích xem nó có phù hợp với project và trường hợp của ta.

Chắc chắn rằng các tính năng trong ES6 giúp ích cải tiến và khiến cho việc code hàng ngày trở nên thú vị hơn, nhưng nếu ta gặp vấn đề về hiệu năng và hay cần xử lý dữ liệu data lớn…Chúng ta nên nghĩ lại về những tools mà chúng ta đang sử dụng.

Với những xử lý nặng, thì ta lựa chọn giải pháp code ít dễ hiểu nhưng lại tối ưu về hiệu năng hơn.

Với khối dữ liệu data lớn, ta nên nghiên cứu và áp dụng cách xử lý tối ưu nhất.

Đối với tất cả trường hợp còn lại, ta sẽ sử dụng ES6 để code trở nên dễ đọc hiểu!


Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *