Grunt and 5 tasks to improve web performance

Posted in Programming on November 15, 2015 by manhhomienbienthuy Comments
Grunt and 5 tasks to improve web performance

Hiệu suất và hoạt động mượt mà là một yếu tố rất quan trọng với 1 trang Web. Trong bài viết này, tôi sẽ hướng dẫn 1 số task của Grunt giúp cải thiệu hiệu suất của trang Web mà cụ thể ở đây là tốc độ tải trang.

Tốc độ tải trang nhanh hay chậm phụ thuộc rất lớn vào lượng dữ liệu cần tải về của trang Web đó. Vì vậy trong bài viết này, tôi sẽ tập trung vào cách giảm dung lượng tải về của trang.

Grunt là gì?

Nếu bạn đang viết những ứng dụng JavaScript hoặc đang phát triển một trang Web, bạn sẽ cần đến các công cụ giúp bạn một số việc như minify CSS, JS, hoặc biên dịch SCSS, CoffeeScript, v.v... Những công việc này hoàn toàn có thể sử dụng những công cụ riêng biệt. Tuy nhiên, nếu chỉ cần đến 1 hoặc 2 công cụ thì có thể dùng riêng được, chứ nếu có nhiều công việc cần làm thì thực hiện chúng một cách riêng rẽ đúng là cực hình. Khi đó bạn cần đến các công cụ có thể chạy các tools đó hoàn toàn tự động.

Một số framework như Rails có cơ chế assets pipeline và có thể đặt rake task. Tuy nhiên không phải lúc nào framework cũng có những công cụ như vậy. Khi đó bạn cần các tools chạy task bên ngoài.

Hiện nay có nhiều công cụ khác nhau, nhưng phổ biến hơn cả là Grunt và Gulp. Grunt có khoảng 30000 download mỗi ngày. Grunt là một tasks runner cho phép bạn cấu hình và gọi các task hoàn toàn tự động.

Grunt là một task runner chạy bằng JavaScript đúng như tên gọi ở trên, nó là một chương trình dùng để gọi các chương trình khác. Những chương trình đó gọi là task của grunt. Thông thường các task này được sử dụng như các công cụ trong quá trình phát triển nên nó còn được gọi là build system.

Grunt là task runner nên nó phụ thuộc vào các plugin để chạy các task của mình. Công việc của nó chỉ đơn giản là gọi và thực thi các task này.

Việc config Grunt có thể hơi khó khi mới bắt đầu. Nhưng nếu đã quen rồi thì mọi chuyện rất đơn giản. Một khi đã config xong, mỗi khi cần chạy, bạn chỉ cần gõ lệnh

$ grunt

là các task sẽ được gọi là thực hiện lần lượt theo config của bạn.

Cài đặt và sử dụng

Grunt là task runner chạy bằng JavaScript nên để chạy được, máy bạn cần được cài Nodejs.

Cài đặt Nodejs

Cài Nodejs bằng lệnh sau:

$ sudo apt-get install nodejs

Sau khi cài Nodejs, Grunt và các plugin của nó có thể dễ dàng cài đặt thông qua npm - trình quản lý package của Nodejs. Grunt version 0.4 yêu cầu Nodejs version từ 0.8 trở nên. Trước khi cài đặt Grunt, có thể bạn cần update npm với lệnh sau:

$ npm update -g npm

Cài Grunt CLI

Để sử dụng lệnh grunt trên Terminal hoặc các trình shell khác, bạn cần cài Grunt CLI bằng lệnh sau. Có thể bạn cần đến sudo tùy vào OS của bạn:

$ npm install -g grunt-cli

Tùy chọn -g ở trên nghĩa là grunt-cli sẽ được cài cho toàn hệ thống. Bạn có thể bỏ nó đi nếu chỉ muốn cài riêng cho project của mình. Nhưng tôi nghĩ bạn nên cài nó để có thể dùng ở tất cả project.

Sau khi cài đặt, bạn có thể gọi lệnh grunt từ shell. Tuy nhiên, việc cài đặt cli hoàn toàn không liên quan đến cài đặt bản thân Grunt task runner. Grunt CLI chỉ có nhiệm vụ là gọi grunt và các task được config ở Grunfile. Với cách làm này, nó cho phép bạn sử dụng nhiều phiên bản grunt khác nhau với các project khác nhau.

Các package được cài bởi npm thường lưu trong thư mục node_packages và bạn có thể ignore nó nếu sử dụng git hoặc các trình quản lý version tương tự.

Việc config Gruntfile tôi sẽ nói ở phần tiếp theo đây.

Config grunt

Config thông thường của Grunt sẽ gồm 2 file: package.json và Gruntfile được đặt ở thư mục gốc project của bạn. Tuy nhiên bạn có thể đặt chúng ở bất cứ đâu tùy theo cấu trúc project. Khi đó, bạn cần sử dụng config grunt.file.setBase để đặt đường dẫn về thư mục gốc của project. Ví dụ tôi đặt Gruntfile và package.json Trong thư mục grunt thì config của tôi sẽ là:

grunt.file.setBase("../")

File package.json là file mà npm dùng để lưu các thông tin liên quan đến project. Nó sẽ có danh sách Grunt và các plugin cần thiết của project trong phần devDependencies.

Gruntfile là file có tên đầy đủ là Gruntfile.js hoặc Gruntfile.coffee và nó dùng để lưu các config chính liên quan đến các task của Grunt. Gruntfile có thể là một trong 2 file trên, .js hoặc .coffee tùy theo nhu cầu của bạn và project của bạn sử dụng JavaScript hay CoffeeScript.

Khi config 2 file trên, chạy npm install nó sẽ tự động cài đặt các package cần thiết để có thể chạy các task của Grunt. Có 1 số cách để tạo file package.json như sau:

  • Sử dụng grunt-init nó sẽ tự động tạo file package.json cho project của bạn, sau khi bạn confirm một số thông tin cần thiết.
  • Sử dụng npm init sẽ tạo ra một file package.json đơn giản.
  • Tùy vào nhu cầu của bạn, bạn có thể tự viết file package.json cho mình hoặc edit file được tạo ra từ 2 cách trên.

Dưới đây là một ví dụ của file package.json

{
  "name": "my-project-name",
  "version": "0.1.0",
  "devDependencies": {
    "grunt": "~0.4.5",
    "grunt-contrib-jshint": "~0.10.0",
    "grunt-contrib-nodeunit": "~0.4.1",
    "grunt-contrib-uglify": "~0.5.0"
  }
}

Config package.json và cài Grunt cùng các plugin

Có một cách khá dễ dàng để cài đặt Grunt và các plugin của nó. Đó là sử dụng lệnh:

$ npm install <module> --save-dev

Sử dụng lệnh trên, không chỉ các gói cần thiết được cài đặt, mà nó còn tự động thêm module vào phần devDependencies của file package.json.

Ví dụ, lệnh dưới đây sẽ cài đặt Grunt và thêm vào devDependencies:

$ npm install grunt --save-dev

Sử dụng lệnh tương tự với các plugin của Grunt. Nếu như bạn làm việc với một project đã có sẵn config của Grunt. Đơn giản, bạn chỉ cần chạy lệnh

$ npm install

trong cùng thư mục với config đó. Nó sẽ tự động đọc file package.json và cài đặt các gói cần thiết đã được định nghĩa trong đó.

Config Gruntfile

Gruntfile (Gruntfile.js hoặc Gruntfile.coffee) là một file JavaScript (hoặc CoffeeScript) dùng để config cho việc hoạt động của Grunt.

Một Gruntfile cần có các thành phần sau:

  • Hàm wrapper
  • Config cho project và các task
  • Load các plugin và tasks
  • Tùy biến task.

Dưới đây là một ví dụ của Gruntfile. Các thông tin của project được đọc từ file package.json. Gruntfile định nghĩa task uglify để nén các file JavaScript. Khi chạy lênh grunt thì default task uglify sẽ được gọi.

module.exports = function(grunt) {
  // Project configuration.
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    uglify: {
      options: {
        banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
      },
      build: {
        src: 'src/<%= pkg.name %>.js',
        dest: 'build/<%= pkg.name %>.min.js'
      }
    }
  });

  // Load the plugin that provides the "uglify" task.
  grunt.loadNpmTasks('grunt-contrib-uglify');

  // Default task(s).
  grunt.registerTask('default', ['uglify']);
};

Bây giờ, tôi sẽ giải thích từng thành phần của Gruntfile thông qua ví dụ trên.

Hàm wrapper

Tất cả các Gruntfile đều được config với mẫu như trên, cho dù nó thay đổi tùy theo project. Bạn có thể tự viết config cho riêng mình, vì suy cho cùng thì đây cũng là code JavaScript mà nó hoạt động như những file JavaScript khác. Tuy nhiên, không nên làm như vậy vì đó không phải là con đường dễ dàng và việc config Grunt cũng không phải là ưu tiên của project.

Hàm wrapper là hàm sẽ bao trọn toàn bộ hoạt động của Grunt. Tất cả các config của Grunt và các task của nó đề đặt ở trong hàm này. Hàm này sẽ như sau:

module.exports = function(grunt) {
  // Các config khác của Grunt đặt ở đây
};

Config của project và các task

Các task Grunt đều phụ thuộc vào config được định nghĩa bên trong grunt.initConfig.

Trong ví dụ của chúng ta, grunt.file.readJSON('package.json') sẽ đọc thông tin JSON từ file package.json. Trong file, các block <% %> là template engine của riêng Grunt và bạn có thể đặt trong đó các biến, tương tự như template engine của các framework lập trình Web thông thường.

Các task của Grunt, trong trường hợp của chúng ta là uglify, cần được config mới có thể hoạt động được. Trong trường hợp của chúng ta, task này nhận tùy chọn là thêm banner lên trên cùng của file kết quả.

// Project configuration.
grunt.initConfig({
  pkg: grunt.file.readJSON('package.json'),
  uglify: {
    options: {
      banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
    },
    build: {
      src: 'src/<%= pkg.name %>.js',
      dest: 'build/<%= pkg.name %>.min.js'
    }
  }
});

Load Grunt và các plugin

Rất nhiều công cụ liên quan đến quá trình phát triển như cssmin, jsmin, linter, v.v... đều có sẵn các plugin của Grunt tương ứng. Và nếu chúng được định nghĩa trong package.json thì chúng sẽ được npm cài đặt.

Để gọi chúng, ở Gruntfile chúng ta cần load Grunt và các plugin này.

// Load the plugin that provides the "uglify" task.
grunt.loadNpmTasks('grunt-contrib-uglify');

Trên đây tôi đã giới thiệu Grunt và cách config chúng cho project của bạn. Tùy vào nhu cầu của bạn, bạn có thể sử dụng nó tùy ý để phục vụ cho công việc của mình. Phần tiếp theo, tôi sẽ giới thiệu một số task của Grunt giúp cải thiệu hiệu suất của trang Web, cụ thể là cải thiện tốc độ tải trang.

Các task grunt giúp cải thiện hiệu suất trang Web

Dưới đây tôi sẽ giới thiệu 5 task thông dụng và rất hữu ích để cải thiện hiệu suất trang Web của bạn.

grunt-contrib-imagemin

Đây là task đầu tiên tôi muốn nói tới. Lý do rất đơn giản, hình ảnh chính là những dữ liệu nặng nhất trên trang Web của chúng ta.

Hãy nhìn vào thống kê của HTTParchive.org và bạn dễ dàng nhận ra rằng, trên 63% lưu lượng truy cập vào trang Web là để tải hình ảnh. Vì lý do đó, nên việc giảm dung lượng hình ảnh có ý nghĩa rất lớn trong việc cải thiện tốc độ tải trang Web của bạn. Và grunt-contrib-imagemin là một task giúp làm việc đó.

Task này sẽ dụng các tools sau để giúp nén ảnh và qua đó, giảm dung lượng file ảnh cần tải.

  • gifsicle để nén các ảnh GIF
  • jpegtran để nén các ảnh JPEG
  • optipng để nén các ảnh PNG
  • svgo để nén các ảnh SVG

Một config đơn giản cho task này như sau:

imagemin: {
  dist: {
    options: {
      optimizationLevel: 5
    },
    files: [{
      expand: true,
      cwd: "src/images",
      src: ["**/*.{png,jpg,gif}"],
      dest: "dist/"
    }]
  }
}

Config này cho phép thiết lập mức độ tối ưu hóa thông qua setting của key optimizationLevel. Giá trị có thể thay đổi trong khoảng từ 0 đến 7, nếu bạn không thiết lập cho nó, giá trị default là 3 sẽ được sử dụng. Trong config trên, tất cả các file có phần mở rộng là png, jpg, gif và nằm trong thư mục src/images sẽ được nén và lưu các file kết quả vào thư mục dist.

Ngoài ra còn một số task khác cũng có tác dụng giảm dung lượng ảnh cho trang Web như:

  • grunt-imageoptim tương tự như grunt-imagemin nhưng chỉ hoạt động trên OS X.
  • grunt-responsive-images tạo file ảnh responsive ở các kích thước khác nhau phù hợp với các loại màn hình khác nhau.
  • grunt-clowncar tác dụng tương tự grunt-responsive-images.
  • grunt-svgmin nén các file svg cũng sử dụng svgo.
  • grunt-tinypng nén ảnh sử dụng dịch vụ tinypng.
  • grunt-spritesmith tạo một sprite sheet cho các ảnh.
  • grunt-webp convert ảnh sang định dang WebP. WebP là định dạng ảnh mới cho phép nén các bức ảnh cả dạng lossless và lossy. Định dạng lossless của WebP có thể nén tới 26% so với file PNG và định dạng lossy thậm chí còn nén được nhiều hơn thế.

grunt-contrib-uglify

Đây là task dùng để minify các file javascript. Task này không chỉ bỏ tất cả các khoảng trắng không cần thiết (bao gồm dấu cách, dấu xuống dòng, v.v...) mà nó còn đổi tên các biến và các hàm để sử dụng những tên ngắn nhất có thể. Điều này có thêm 1 lợi ích đó là các file javascript sau khi uglify sẽ rất khó đọc, cho dù có dùng các tools beautify cũng không thể khôi phục lại trạng thái ban đầu vì khi đó, các hàm và biến chỉ còn là những cái tên vô nghĩa như a, b, c, v.v...

Một vài tùy chọn cho task này đó là sourceMap và banner. Tùy chọn sourceMap sẽ tạo một file map trong cùng thư mục với file đích được tạo ra. Default của option này là false, để kích hoạt nó, bạn set nó là true là được. Tùy chọn banner sẽ chèn thêm một đoạn comment vào đầu file đích. Bạn có thể viết vào đó giải thích ngắn gọn, tên tác giả, version, license, v.v...

Dưới đây là một config đơn giản cho task này.

uglify: {
  dist: {
    options: {
      sourceMap: true,
      banner: "/*! Copyright: manhhomienbienthuy */"
    },
    files: {
      'dest/output.min.js': ['src/input.js'],
    }
  }
}

Để mình hoạ cho cách làm của task này, tôi có 1 ví dụ với đoạn javaScript sau:

var MyApplication = function() {
  var data = 'hello';

  this.sum = function(first, second) {
    return first + second;
  }

  this.showData = function() {
    return data;
  }
};

Sau khi dùng uglify, kết quả thu được sẽ là

var MyApplication=function(){var a="hello";this.sum=function(a,b){return a+b},this.showData=function(){return a}};

Ngoài ra có một số task khác giúp tối ưu hóa Javascript:

grunt-contrib-cssmin

Tên của task này đã nói lên tất cả, task này sẽ nén các file css. Công việc của nó là xóa bỏ tất cả các dấu cách thừa trong file css. Cũng tương tự như uglify nó cũng có tùy chọn banner.

Một config đơn giản cho task này như sau:

cssmin: {
  dist: {
    options: {
      banner: "/*! Copyright: manhhomienbienthuy */"
    },
    files: {
      'dist/css/style.min.css': ['src/css/**/*.css']
    }
  }
}

Với config trên, cssmin sẽ minify tất cả các file css trong thư mục src/css và lưu kết quả vào 1 file duy nhất style.min.css nằm trong thư mục 'dist/css'. Config trên cũng thêm banner vào đầu file min này.

Ngoài ra cũng có một số task khác cũng tăng hiệu suất trang Web của bạn như:

  • grunt-inline-css chuyển các file css rời thành inline css. Việc sử dụng inline css sẽ giúp trình duyệt render nội dung nhanh hơn do không phải load css thêm nữa.
  • grunt-combine-media-queries giúp bạn tổng hợp các media query giống nhau vào trong cùng một câu media query.
  • grunt-revizor nén code css bằng cách thu gọn tên của css selector. Ví dụ .b-tabmenu--item__active-- -> .zS, #success_info-- -> .e6

Ngoài ra có một tools không phải của grunt có thể thu gọn code css. Ví dụ:

a {
  font-family: Arial;
  font-style: italic;
  font-size: 14px;
  line-height: 18px;
  font-weight: bold;
  background-image: url('example.png');
  background-color: red;
  background-size: cover;
  background-repeat: no-repeat;
}

=>

a {
  font: italic bold 14px/18px Arial;
  background: red url('example.png') no-repeat / cover;
}

grunt-uncss

Một task khác cũng thao tác với các file css đó là grunt-uncss. Task này sẽ loại bỏ tất cả các code css không được dùng từ trong các file của project của bạn. Do đó, nó sẽ giảm kích thước các file css và giảm thời gian tải trang. Điều này thực sự hữu ích khi bạn sẽ dụng các template dựng sẵn của người khác hoặc dùng những framework như Bootstrap hay Foundation. Không phải tất cả code css của họ bạn đều dùng, và giữ chúng lại chỉ làm chậm quá trình tải trang.

Có một số giới hạn mà bạn cần tìm hiểu trước ở tài liệu của uncss

Một demo ở trên trang github của uncss cho thấy rằng, nó có thể giảm dung lượng css của Bootstrap từ 120Kb xuống còn 11Kb bằng cách loại bỏ hết các thành phần không được sử dụng.

uncss

Có một số tùy chọn khi config task này, ví dụ như ignore cho phép chúng ta giữ lại những thành phần cho dù hiện tại nó chưa được sử dụng. Hoặc tùy chọn ignoreSheets cho phép chúng ta giữ lại toàn bộ các file chưa được dùng.

Uncss có thể kiểm tra cả những thành phần được render bằng javascript, bằng cách chạy javascript của các trang thông qua PhantomJS. Tuy nhiên, nếu những render này lấy dữ liệu từ AJAX thì nó chưa làm được. Ngoài ra, có một hạn chế là nó không tương thích với các template engine của các framework, nó chỉ làm việc với các file html mà thôi.

Một config ví dụ như sau:

uncss: {
  dist: {
    options: {
      ignore: [/js-.+/, '.special-class'],
      ignoreSheets: [/fonts.googleapis/],
    },
    files: {
      'dist/css/unused-removed.css': ['src/index.html', 'src/contact.html', 'src/service.html']
    }
  }
}

Ngoài grunt-uncss được giới thiệu trên đây, có một task khác là grunt-ucss cũng có tác dụng tìm những thành phần css không được sử dụng ở trang. Tuy nhiên, grunt-ucss có hạn chế là nó không loại bỏ những thành phần này cho chúng ta.

grunt-contrib-htmlmin

Đây là task cuối cùng mà tôi muốn đề cập đến, đó là htmlmin, nó sẽ minify html code. Nó không thực sự giúp ích được nhiều, bởi vì trong quá trình phát triển, chúng ta không thể minify các file HTML được. Và cho dù minify thì nó cũng chỉ có thể giảm một vài Kb mà thôi. Tuy nhiên, một vài Kb với mỗi người nhưng nhiều người dùng thì nó cũng có ý nghĩa hết sức quan trọng.

Dưới đây là một ví dụ về config của task này:

htmlmin: {
  dist: {
    options: {
      removeComments: true,
      collapseWhitespace: true
    },
    files: [{
      expand: true,
      cwd: 'src',
      src: '**/*.html',
      dest: 'dist/'
    }]
  }
}

Với config trên, tất cả các file html trong thư mục src sẽ được minify. Với mỗi file này, htmlmin sẽ xóa tất cả các comment và thu lượng khoảng trắng về mức tối đa, và kết quả được lưu ở dist.

Ngoài những task đã kể trên, có một số task cũng giúp tăng hiệu suất trang Web của bạn như:

  • grunt-contrib-concat dùng để nối nhiều file thành 1 file.
  • grunt-contrib-compress dùng để nén file và thư mục.
  • grunt-zopfli cũng dùng để nén file, thư mục nhưng sử dụng thuật toán Zopfli. Thuật toán Zopfli có thể nén tốt hơn 3-8% so với zlib.
  • grunt-reduce tự động hóa việc tối ưu toàn bộ các file tĩnh (css, js, ảnh, v.v...)

Kết luận

Trong bài viết này, tôi chỉ tập trung vào các task giúp giảm dung lượng những dữ liệu tĩnh để cải thiện tốc độ tải trang Web của bạn. Chúng rất đơn giản, dễ dùng và kết quả thu được biểu hiện rõ ràng ngay trước mắt chúng ta. Hy vọng bài viết có ích cho các bạn đang muốn phát triển Web cho riêng mình :)

Đây là config đầy đủ của Gruntjs cho cả 5 task trên.

Gruntfile.js

module.exports = function(grunt) {

  grunt.initConfig({
    pkg: grunt.file.readJSON("package.json"),
    imagemin: {
      dist: {
        options: {
          optimizationLevel: 5
        },
        files: [{
           expand: true,
           cwd: "static_root",
           src: ["**/*.{png,jpg,gif,ico}"],
           dest: "static_root"
        }]
      }
    },
    uglify: {
      dist: {
        options: {
           sourceMap: false,
           banner: "/*! Copyright: manhhomienbienthuy */"
        },
        files: [{
           expand: true,
           cwd: "static_root",
           src: ["**/*.js"],
           dest: "static_root"
        }]
      }
    },
    cssmin: {
      dist: {
        options: {
           banner: "/*! Copyright: manhhomienbienthuy */"
        },
        files: [{
           expand: true,
           cwd: "static_root",
           src: ["**/*.css"],
           dest: "static_root"
        }]
      }
    },
    uncss: {
      dist: {
        files: {
           "static_root/css/unused-removed.css": ["templates/*.html"]
        }
      }
    },
    htmlmin: {
      dist: {
        options: {
           removeComments: true,
           collapseWhitespace: true
        },
        files: [{
           expand: true,
           cwd: "templates",
           src: "**/*.html",
           dest: "static_root/"
        }]
      }
    }
  });

  grunt.loadNpmTasks("grunt-contrib-imagemin");
  grunt.loadNpmTasks("grunt-contrib-uglify");
  grunt.loadNpmTasks("grunt-contrib-cssmin");
  grunt.loadNpmTasks("grunt-uncss");
  grunt.loadNpmTasks("grunt-contrib-htmlmin");
  grunt.file.setBase('../')
  grunt.registerTask("default", ["imagemin", "uglify", "cssmin", "uncss", "htmlmin"]);
};

package.json

{
  "name": "project",
  "version": "1.0.0",
  "description": "This is a project",
  "main": "Gruntfile.js",
  "dependencies": {
    "grunt": "^0.4.5"
  },
  "devDependencies": {
    "grunt-contrib-uglify": "^0.9.2",
    "grunt-contrib-cssmin": "^0.13.0",
    "grunt-contrib-imagemin": "^0.9.4",
    "grunt-uncss": "^0.4.1",
    "grunt-contrib-htmlmin": "^0.4.0"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "***.git"
  },
  "author": "manhhomienbienthuy",
  "license": "MIT"
}

I apologise for any typos. If you notice a problem, please let me know.

Thank you all for your attention.