Bug 1414728 - [Experiment] Implement RequestList with fixed-data-table-2 draft
authorRicky Chien <ricky060709@gmail.com>
Fri, 03 Nov 2017 14:32:33 +0800
changeset 693408 6f833f70ecbf33b64e12fe81538e74978e095c8a
parent 693376 179dae92e4d794e7f45ad080ff01908c80691f31
child 739017 c74f7e9aa3cd498a4fa25f5f962b5a94954d8098
push id87785
push userbmo:rchien@mozilla.com
push dateMon, 06 Nov 2017 06:46:19 +0000
bugs1414728
milestone58.0a1
Bug 1414728 - [Experiment] Implement RequestList with fixed-data-table-2 MozReview-Commit-ID: J7nSlqh1GgA
devtools/client/netmonitor/index.js
devtools/client/netmonitor/package.json
devtools/client/netmonitor/src/assets/styles/fixed-data-table.css
devtools/client/netmonitor/src/assets/styles/netmonitor.css
devtools/client/netmonitor/src/components/MonitorPanel.js
devtools/client/netmonitor/src/components/RequestList.js
devtools/client/netmonitor/src/components/RequestListCellCause.js
devtools/client/netmonitor/src/components/RequestListCellDomain.js
devtools/client/netmonitor/src/components/RequestListCellFile.js
devtools/client/netmonitor/src/components/RequestListCellMethod.js
devtools/client/netmonitor/src/components/RequestListCellScheme.js
devtools/client/netmonitor/src/components/RequestListCellStatus.js
devtools/client/netmonitor/src/components/RequestListCellType.js
devtools/client/netmonitor/src/components/RequestListColumnCause.js
devtools/client/netmonitor/src/components/RequestListColumnDomain.js
devtools/client/netmonitor/src/components/RequestListColumnFile.js
devtools/client/netmonitor/src/components/RequestListColumnMethod.js
devtools/client/netmonitor/src/components/RequestListColumnScheme.js
devtools/client/netmonitor/src/components/RequestListColumnStatus.js
devtools/client/netmonitor/src/components/RequestListColumnType.js
devtools/client/netmonitor/src/components/RequestListEmptyNotice.js
devtools/client/netmonitor/src/components/RequestListHeader.js
devtools/client/netmonitor/src/components/RequestListHeaders.js
devtools/client/netmonitor/src/middleware/batching.js
devtools/client/netmonitor/yarn.lock
--- a/devtools/client/netmonitor/index.js
+++ b/devtools/client/netmonitor/index.js
@@ -33,16 +33,17 @@ pref("devtools.netmonitor.har.jsonpCallb
 pref("devtools.netmonitor.har.includeResponseBodies", true);
 pref("devtools.netmonitor.har.compress", false);
 pref("devtools.netmonitor.har.forceExport", false);
 pref("devtools.netmonitor.har.pageLoadedTimeout", 1500);
 pref("devtools.netmonitor.har.enableAutoExportToFile", false);
 pref("devtools.netmonitor.persistlog", false);
 pref("devtools.styleeditor.enabled", true);
 
+require("./src/assets/styles/fixed-data-table.css");
 require("./src/assets/styles/netmonitor.css");
 
 const EventEmitter = require("devtools-modules/src/utils/event-emitter");
 EventEmitter.decorate(window);
 
 const { configureStore } = require("./src/utils/create-store");
 const App = require("./src/components/App");
 const { Connector } = require("./src/connector/index");
--- a/devtools/client/netmonitor/package.json
+++ b/devtools/client/netmonitor/package.json
@@ -7,19 +7,21 @@
   "description": "Network monitor in developer tools",
   "dependencies": {
     "codemirror": "^5.24.2",
     "devtools-config": "=0.0.12",
     "devtools-contextmenu": "=0.0.3",
     "devtools-launchpad": "=0.0.103",
     "devtools-modules": "=0.0.32",
     "devtools-source-editor": "=0.0.3",
+    "fixed-data-table-2": "^0.8.5",
     "immutable": "^3.8.1",
     "jszip": "^3.1.3",
     "react": "=15.6.1",
+    "react-dimensions": "^1.3.1",
     "react-dom": "=15.6.1",
     "react-redux": "=5.0.3",
     "redux": "^3.6.0",
     "reselect": "^3.0.1"
   },
   "devDependencies": {
     "babel-register": "^6.24.0",
     "file-loader": "^0.10.1"
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/assets/styles/fixed-data-table.css
@@ -0,0 +1,560 @@
+/**
+ * FixedDataTable v0.8.5 
+ *
+ * Copyright Schrodinger, LLC
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+/**
+ * Copyright Schrodinger, LLC
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule fixedDataTableCellGroupLayout
+ */
+
+.fixedDataTableCellGroupLayout_cellGroup {
+  -webkit-backface-visibility: hidden;
+          backface-visibility: hidden;
+  left: 0;
+  overflow: hidden;
+  position: absolute;
+  top: 0;
+  white-space: nowrap;
+}
+
+.fixedDataTableCellGroupLayout_cellGroup > .public_fixedDataTableCell_main {
+  display: inline-block;
+  vertical-align: top;
+  white-space: normal;
+}
+
+.fixedDataTableCellGroupLayout_cellGroupWrapper {
+  position: absolute;
+  top: 0;
+}
+/**
+ * Copyright Schrodinger, LLC
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule fixedDataTableCellLayout
+ */
+
+.fixedDataTableCellLayout_main {
+  border-right-style: solid;
+  border-right-width: 1px;
+  border-width: 0 1px 0 0;
+  -webkit-box-sizing: border-box;
+          box-sizing: border-box;
+  display: block;
+  overflow: hidden;
+  position: absolute;
+  white-space: normal;
+}
+
+.fixedDataTableCellLayout_lastChild {
+  border-width: 0 1px 1px 0;
+}
+
+.fixedDataTableCellLayout_alignRight {
+  text-align: right;
+}
+
+.fixedDataTableCellLayout_alignCenter {
+  text-align: center;
+}
+
+.fixedDataTableCellLayout_wrap1 {
+  display: table;
+}
+
+.fixedDataTableCellLayout_wrap2 {
+  display: table-row;
+}
+
+.fixedDataTableCellLayout_wrap3 {
+  display: table-cell;
+  vertical-align: middle;
+}
+
+.fixedDataTableCellLayout_columnResizerContainer {
+  position: absolute;
+  right: 0px;
+  width: 6px;
+  z-index: 1;
+}
+
+.fixedDataTableCellLayout_columnResizerContainer:hover {
+  cursor: ew-resize;
+}
+
+.fixedDataTableCellLayout_columnResizerContainer:hover .fixedDataTableCellLayout_columnResizerKnob {
+  visibility: visible;
+}
+
+.fixedDataTableCellLayout_columnResizerKnob {
+  position: absolute;
+  right: 0px;
+  visibility: hidden;
+  width: 4px;
+}
+/**
+ * Copyright Schrodinger, LLC
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule fixedDataTableColumnResizerLineLayout
+ */
+
+.fixedDataTableColumnResizerLineLayout_mouseArea {
+  cursor: ew-resize;
+  position: absolute;
+  right: -5px;
+  width: 12px;
+}
+
+.fixedDataTableColumnResizerLineLayout_main {
+  border-right-style: solid;
+  border-right-width: 1px;
+  -webkit-box-sizing: border-box;
+          box-sizing: border-box;
+  position: absolute;
+  z-index: 10;
+}
+
+body[dir="rtl"] .fixedDataTableColumnResizerLineLayout_main {
+  /* the resizer line is in the wrong position in RTL with no easy fix.
+   * Disabling is more useful than displaying it.
+   * #167 (github) should look into this and come up with a permanent fix.
+   */
+  display: none !important;
+}
+
+.fixedDataTableColumnResizerLineLayout_hiddenElem {
+  display: none !important;
+}
+/**
+ * Copyright Schrodinger, LLC
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule fixedDataTableLayout
+ */
+
+.fixedDataTableLayout_main {
+  border-style: solid;
+  border-width: 1px;
+  -webkit-box-sizing: border-box;
+          box-sizing: border-box;
+  overflow: hidden;
+  position: relative;
+}
+
+.fixedDataTableLayout_header,
+.fixedDataTableLayout_hasBottomBorder {
+  border-bottom-style: solid;
+  border-bottom-width: 1px;
+}
+
+.fixedDataTableLayout_footer .public_fixedDataTableCell_main {
+  border-top-style: solid;
+  border-top-width: 1px;
+}
+
+.fixedDataTableLayout_topShadow,
+.fixedDataTableLayout_bottomShadow {
+  height: 4px;
+  left: 0;
+  position: absolute;
+  right: 0;
+  z-index: 1;
+}
+
+.fixedDataTableLayout_bottomShadow {
+  margin-top: -4px;
+}
+
+.fixedDataTableLayout_rowsContainer {
+  overflow: hidden;
+  position: relative;
+}
+
+.fixedDataTableLayout_horizontalScrollbar {
+  bottom: 0;
+  position: absolute;
+}
+/**
+ * Copyright Schrodinger, LLC
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule fixedDataTableRowLayout
+ */
+
+.fixedDataTableRowLayout_main {
+  -webkit-box-sizing: border-box;
+          box-sizing: border-box;
+  overflow: hidden;
+  position: absolute;
+  top: 0;
+}
+
+.fixedDataTableRowLayout_body {
+  left: 0;
+  position: absolute;
+  top: 0;
+}
+
+.fixedDataTableRowLayout_rowExpanded {
+  -webkit-box-sizing: border-box;
+          box-sizing: border-box;
+  left: 0;
+  position: absolute;
+}
+
+.fixedDataTableRowLayout_fixedColumnsDivider {
+  -webkit-backface-visibility: hidden;
+          backface-visibility: hidden;
+  border-left-style: solid;
+  border-left-width: 1px;
+  left: 0;
+  position: absolute;
+  top: 0;
+  width: 0;
+}
+
+.fixedDataTableRowLayout_columnsShadow {
+  position: absolute;
+  width: 4px;
+}
+
+.fixedDataTableRowLayout_columnsRightShadow {
+  right: 1px;
+}
+
+.fixedDataTableRowLayout_rowWrapper {
+  position: absolute;
+  top: 0;
+}
+/**
+ * Copyright Schrodinger, LLC
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ScrollbarLayout
+ */
+
+.ScrollbarLayout_main {
+  -webkit-box-sizing: border-box;
+          box-sizing: border-box;
+  outline: none;
+  overflow: hidden;
+  position: absolute;
+  -webkit-user-select: none;
+     -moz-user-select: none;
+      -ms-user-select: none;
+          user-select: none;
+}
+
+.ScrollbarLayout_mainVertical {
+  bottom: 0;
+  right: 0;
+  top: 0;
+  width: 15px;
+}
+
+.ScrollbarLayout_mainHorizontal {
+  bottom: 0;
+  height: 15px;
+  left: 0;
+  -webkit-transition-property: background-color height;
+  transition-property: background-color height;
+}
+
+/* Touching the scroll-track directly makes the scroll-track bolder */
+.ScrollbarLayout_mainHorizontal.public_Scrollbar_mainActive,
+.ScrollbarLayout_mainHorizontal:hover {
+  height: 17px;
+}
+
+.ScrollbarLayout_face {
+  left: 0;
+  overflow: hidden;
+  position: absolute;
+  z-index: 1;
+  -webkit-transition-duration: 250ms;
+          transition-duration: 250ms;
+  -webkit-transition-timing-function: ease;
+          transition-timing-function: ease;
+  -webkit-transition-property: background-color width position;
+  transition-property: background-color width position;
+}
+
+/**
+ * This selector renders the "nub" of the scrollface. The nub must
+ * be rendered as pseudo-element so that it won't receive any UI events then
+ * we can get the correct `event.offsetX` and `event.offsetY` from the
+ * scrollface element while dragging it.
+ */
+.ScrollbarLayout_face:after {
+  border-radius: 6px;
+  content: '';
+  display: block;
+  position: absolute;
+  -webkit-transition: background-color 250ms ease;
+  transition: background-color 250ms ease;
+}
+
+.ScrollbarLayout_faceHorizontal {
+  bottom: 0;
+  left: 0;
+  top: 0;
+}
+
+.ScrollbarLayout_faceHorizontal:after {
+  bottom: 4px;
+  left: 0;
+  top: 4px;
+  width: 100%;
+}
+
+.ScrollbarLayout_faceHorizontal.public_Scrollbar_faceActive:after,
+.ScrollbarLayout_main:hover .ScrollbarLayout_faceHorizontal:after {
+  bottom: calc(4px/2);
+}
+
+.ScrollbarLayout_faceVertical {
+  left: 0;
+  right: 0;
+  top: 0;
+}
+
+.ScrollbarLayout_faceVertical:after {
+  height: 100%;
+  left: 4px;
+  right: 4px;
+  top: 0;
+}
+
+.ScrollbarLayout_main:hover .ScrollbarLayout_faceVertical:after,
+.ScrollbarLayout_faceVertical.public_Scrollbar_faceActive:after {
+  left: calc(4px/2);
+  right: calc(4px/2);
+}
+/**
+ * Copyright Schrodinger, LLC
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule fixedDataTable
+ *
+ */
+
+/**
+ * Table.
+ */
+.public_fixedDataTable_main {
+  border-color: #d3d3d3;
+}
+
+.public_fixedDataTable_header,
+.public_fixedDataTable_hasBottomBorder {
+  border-color: #d3d3d3;
+}
+
+.public_fixedDataTable_header .public_fixedDataTableCell_main {
+  font-weight: bold;
+}
+
+.public_fixedDataTable_header,
+.public_fixedDataTable_header .public_fixedDataTableCell_main {
+  background-color: #f6f7f8;
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#efefef));
+  background-image: linear-gradient(#fff, #efefef);
+}
+
+.public_fixedDataTable_footer .public_fixedDataTableCell_main {
+  background-color: #f6f7f8;
+  border-color: #d3d3d3;
+}
+
+.public_fixedDataTable_topShadow {
+  background: 0 0 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAECAYAAABP2FU6AAAAF0lEQVR4AWPUkNeSBhHCjJoK2twgFisAFagCCp3pJlAAAAAASUVORK5CYII=) repeat-x;
+}
+
+.public_fixedDataTable_bottomShadow {
+  background: 0 0 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAECAYAAABP2FU6AAAAHElEQVQI12MwNjZmZdAT1+Nm0JDWEGZQk1GTBgAWkwIeAEp52AAAAABJRU5ErkJggg==) repeat-x;
+}
+
+.public_fixedDataTable_horizontalScrollbar .public_Scrollbar_mainHorizontal {
+  background-color: #fff;
+}
+/**
+ * Copyright Schrodinger, LLC
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule fixedDataTableCell
+ */
+
+/**
+ * Table cell.
+ */
+.public_fixedDataTableCell_main {
+  background-color: #fff;
+  border-color: #d3d3d3;
+}
+
+.public_fixedDataTableCell_highlighted {
+  background-color: #f4f4f4;
+}
+
+.public_fixedDataTableCell_cellContent {
+  padding: 8px;
+}
+
+.public_fixedDataTableCell_columnResizerKnob {
+  background-color: #0284ff;
+}
+.public_fixedDataTableCell_hasReorderHandle .public_fixedDataTableCell_cellContent {
+	margin-left: 12px;
+}
+/**
+ * Column reorder goodies.
+ */
+.fixedDataTableCellLayout_columnReorderContainer {
+  border-color: #0284ff;
+  background-color: rgba(0,0,0,0.1);
+  width: 12px;
+  margin-right: -12px;
+  float: left;
+  cursor: move;
+}
+.fixedDataTableCellLayout_columnReorderContainer:after {
+	content: '::';
+	position: absolute;
+	top: 50%;
+	left: 1px;
+	-webkit-transform: translateY(-50%);
+	        transform: translateY(-50%);
+}
+/**
+ * Copyright Schrodinger, LLC
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule fixedDataTableColumnResizerLine
+ *
+ */
+
+/**
+ * Column resizer line.
+ */
+.public_fixedDataTableColumnResizerLine_main {
+  border-color: #0284ff;
+}
+/**
+ * Copyright Schrodinger, LLC
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule fixedDataTableRow
+ */
+
+/**
+ * Table row.
+ */
+.public_fixedDataTableRow_main {
+  background-color: #fff;
+}
+
+.public_fixedDataTableRow_highlighted,
+.public_fixedDataTableRow_highlighted .public_fixedDataTableCell_main {
+  background-color: #f6f7f8;
+}
+
+.public_fixedDataTableRow_fixedColumnsDivider {
+  border-color: #d3d3d3;
+}
+
+.public_fixedDataTableRow_columnsShadow {
+  background: 0 0 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAABCAYAAAD5PA/NAAAAFklEQVQIHWPSkNeSBmJhTQVtbiDNCgASagIIuJX8OgAAAABJRU5ErkJggg==) repeat-y;
+}
+
+.public_fixedDataTableRow_columnsRightShadow {
+  -webkit-transform: rotate(180deg);
+          transform: rotate(180deg);
+}
+/**
+ * Copyright Schrodinger, LLC
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule Scrollbar
+ *
+ */
+
+/**
+ * Scrollbars.
+ */
+
+/* Touching the scroll-track directly makes the scroll-track bolder */
+.public_Scrollbar_main.public_Scrollbar_mainActive,
+.public_Scrollbar_main {
+  background-color: #fff;
+  border-left: 1px solid #d3d3d3;
+}
+
+.public_Scrollbar_mainOpaque,
+.public_Scrollbar_mainOpaque.public_Scrollbar_mainActive,
+.public_Scrollbar_mainOpaque:hover {
+  background-color: #fff;
+}
+
+.public_Scrollbar_face:after {
+  background-color: #c2c2c2;
+}
+
+.public_Scrollbar_main:hover .public_Scrollbar_face:after,
+.public_Scrollbar_mainActive .public_Scrollbar_face:after,
+.public_Scrollbar_faceActive:after {
+  background-color: #7d7d7d;
+}
--- a/devtools/client/netmonitor/src/assets/styles/netmonitor.css
+++ b/devtools/client/netmonitor/src/assets/styles/netmonitor.css
@@ -194,24 +194,16 @@ body,
 .requests-list-reload-notice-button {
   font-size: inherit;
   min-height: 26px;
   margin: 0 5px;
 }
 
 /* Requests list table */
 
-.request-list-container {
-  display: flex;
-  flex-direction: column;
-  width: 100%;
-  height: 100%;
-  overflow: hidden;
-}
-
 .requests-list-wrapper {
   width: 100%;
   height: 100%;
 }
 
 .requests-list-table {
   display: table;
   position: relative;
@@ -228,25 +220,23 @@ body,
   right: 0;
   overflow-x: hidden;
   overflow-y: auto;
   --timings-scale: 1;
   --timings-rev-scale: 1;
 }
 
 .requests-list-column {
-  display: table-cell;
   cursor: default;
   text-align: center;
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
   vertical-align: middle;
-  max-width: 50px;
-  min-width: 50px;
+  line-height: 24px;
 }
 
 .requests-list-column > * {
   display: inline-block;
 }
 
 .theme-firebug .requests-list-column {
   padding: 1px;
@@ -268,16 +258,21 @@ body,
   padding: 0;
   width: 100%;
 }
 
 .requests-list-headers .requests-list-column:first-child .requests-list-header-button {
   border-width: 0;
 }
 
+.requests-list-header {
+  line-height: 24px;
+  text-align: center;
+}
+
 .requests-list-header-button {
   background-color: transparent;
   border-image: linear-gradient(transparent 15%,
                                 var(--theme-splitter-color) 15%,
                                 var(--theme-splitter-color) 85%,
                                 transparent 85%) 1 1;
   border-width: 0;
   border-inline-start-width: 1px;
@@ -365,17 +360,16 @@ body,
                                     transparent);
 }
 
 /* Requests list column */
 
 /* Status column */
 
 .requests-list-status {
-  width: 8%;
    /* Don't ellipsize status codes */
   text-overflow: initial;
 }
 
 .theme-firebug .requests-list-status {
   font-weight: bold;
 }
 
@@ -435,117 +429,45 @@ body,
 .requests-list-status-icon[data-code^="5"] {
   background-color: var(--theme-highlight-pink);
   border-radius: 0;
   transform: rotate(45deg);
 }
 
 /* Method column */
 
-.requests-list-method {
-  width: 8%;
-}
-
 .theme-firebug .requests-list-method {
   color: rgb(128, 128, 128);
 }
 
 /* File column */
 
-.requests-list-file {
-  width: 22%;
-}
-
 .requests-list-file.requests-list-column {
   text-align: start;
 }
 
 .requests-list-icon {
   background: transparent;
   width: 15px;
   height: 15px;
   margin: 0 4px;
   outline: 1px solid var(--table-splitter-color);
-  vertical-align: top;
-}
-
-/* Protocol column */
-
-.requests-list-protocol {
-  width: 8%;
-}
-
-/* Cookies column */
-
-.requests-list-cookies {
-  width: 6%;
-}
-
-/* Set Cookies column */
-
-.requests-list-set-cookies {
-  width: 8%;
-}
-
-/* Scheme column */
-
-.requests-list-scheme {
-  width: 8%;
-}
-
-/* Domain column */
-
-.requests-list-domain {
-  width: 13%;
-}
-
-/* Start Time column */
-
-.requests-list-start-time {
-  width: 8%;
-}
-
-/* End Time column */
-
-.requests-list-end-time {
-  width: 8%;
-}
-
-/* Response Time column */
-
-.requests-list-response-time {
-  width: 10%;
-}
-
-/* Duration column */
-
-.requests-list-duration {
-  width: 8%;
-}
-
-/* Latency column */
-
-.requests-list-latency {
-  width: 8%;
-}
-
-.requests-list-response-header {
-  width: 13%;
+  vertical-align: middle;
 }
 
 .requests-list-domain.requests-list-column {
   text-align: start;
 }
 
 .requests-security-state-icon {
   display: inline-block;
   width: 16px;
   height: 16px;
   margin: 0 4px;
-  vertical-align: top;
+  vertical-align: middle;
 }
 
 .request-list-item.selected .requests-security-state-icon {
   filter: brightness(1.3);
 }
 
 .security-state-insecure {
   background-image: url(chrome://devtools/skin/images/security-state-insecure.svg);
@@ -562,59 +484,31 @@ body,
 .security-state-broken {
   background-image: url(chrome://devtools/skin/images/security-state-broken.svg);
 }
 
 .security-state-local {
   background-image: url(chrome://devtools/skin/images/globe.svg);
 }
 
-/* RemoteIP column */
-
-.requests-list-remoteip {
-  width: 9%;
-}
-
-/* Cause column */
-
-.requests-list-cause {
-  width: 9%;
-}
-
 .requests-list-cause-stack {
   display: inline-block;
   background-color: var(--theme-body-color-alt);
   color: var(--theme-body-background);
   font-size: 8px;
   font-weight: bold;
   line-height: 10px;
   border-radius: 3px;
   padding: 0 2px;
   margin: 0;
   margin-inline-end: 3px;
 }
 
-/* Type column */
-
-.requests-list-type {
-  width: 6%;
-}
-
-/* Transferred column */
-
-.requests-list-transferred {
-  width: 9%;
-}
-
 /* Size column */
 
-.requests-list-size {
-  width: 7%;
-}
-
 .theme-firebug .requests-list-size {
   justify-content: end;
   padding-inline-end: 4px;
 }
 
 /* Waterfall column */
 
 .requests-list-waterfall {
--- a/devtools/client/netmonitor/src/components/MonitorPanel.js
+++ b/devtools/client/netmonitor/src/components/MonitorPanel.js
@@ -10,17 +10,16 @@ const {
   createFactory,
   DOM,
   PropTypes,
 } = require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
 const Actions = require("../actions/index");
 const { updateFormDataSections } = require("../utils/request-utils");
-const { getSelectedRequest } = require("../selectors/index");
 
 // Components
 const SplitBox = createFactory(require("devtools/client/shared/components/splitter/SplitBox"));
 const NetworkDetailsPanel = createFactory(require("./NetworkDetailsPanel"));
 const RequestList = createFactory(require("./RequestList"));
 const Toolbar = createFactory(require("./Toolbar"));
 const { div } = DOM;
 const MediaQueryList = window.matchMedia("(min-width: 700px)");
@@ -28,20 +27,18 @@ const MediaQueryList = window.matchMedia
 /**
  * Monitor panel component
  * The main panel for displaying various network request information
  */
 class MonitorPanel extends Component {
   static get propTypes() {
     return {
       connector: PropTypes.object.isRequired,
-      isEmpty: PropTypes.bool.isRequired,
       networkDetailsOpen: PropTypes.bool.isRequired,
       openNetworkDetails: PropTypes.func.isRequired,
-      request: PropTypes.object,
       sourceMapService: PropTypes.object,
       openLink: PropTypes.func,
       updateRequest: PropTypes.func.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
@@ -80,17 +77,16 @@ class MonitorPanel extends Component {
     this.setState({
       isVerticalSpliter: MediaQueryList.matches,
     });
   }
 
   render() {
     let {
       connector,
-      isEmpty,
       networkDetailsOpen,
       openLink,
       sourceMapService,
     } = this.props;
 
     let initialWidth = Services.prefs.getIntPref(
         "devtools.netmonitor.panes-network-details-width");
     let initialHeight = Services.prefs.getIntPref(
@@ -101,17 +97,17 @@ class MonitorPanel extends Component {
         Toolbar(),
         SplitBox({
           className: "devtools-responsive-container",
           initialWidth: `${initialWidth}px`,
           initialHeight: `${initialHeight}px`,
           minSize: "50px",
           maxSize: "80%",
           splitterSize: "1px",
-          startPanel: RequestList({ isEmpty, connector }),
+          startPanel: RequestList({ connector }),
           endPanel: networkDetailsOpen && NetworkDetailsPanel({
             ref: "endPanel",
             connector,
             openLink,
             sourceMapService,
           }),
           endPanelCollapsed: !networkDetailsOpen,
           endPanelControl: true,
@@ -119,17 +115,15 @@ class MonitorPanel extends Component {
         }),
       )
     );
   }
 }
 
 module.exports = connect(
   (state) => ({
-    isEmpty: state.requests.requests.isEmpty(),
     networkDetailsOpen: state.ui.networkDetailsOpen,
-    request: getSelectedRequest(state),
   }),
   (dispatch) => ({
     openNetworkDetails: (open) => dispatch(Actions.openNetworkDetails(open)),
     updateRequest: (id, data, batch) => dispatch(Actions.updateRequest(id, data, batch)),
   }),
 )(MonitorPanel);
--- a/devtools/client/netmonitor/src/components/RequestList.js
+++ b/devtools/client/netmonitor/src/components/RequestList.js
@@ -1,42 +1,158 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {
+  Component,
+  PropTypes,
   createFactory,
-  DOM,
-  PropTypes,
 } = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const Dimensions = require("react-dimensions");
+const {
+  getDisplayedRequests,
+  getWaterfallScale,
+} = require("../selectors/index");
 
 // Components
-const RequestListContent = createFactory(require("./RequestListContent"));
+const Table = createFactory(require("fixed-data-table-2").Table);
+const Column = createFactory(require("fixed-data-table-2").Column);
+const RequestListCellStatus = createFactory(require("./RequestListCellStatus"));
+const RequestListCellMethod = createFactory(require("./RequestListCellMethod"));
+const RequestListCellFile = createFactory(require("./RequestListCellFile"));
+const RequestListCellScheme = createFactory(require("./RequestListCellScheme"));
+const RequestListCellCause = createFactory(require("./RequestListCellCause"));
+const RequestListCellType = createFactory(require("./RequestListCellType"));
+const RequestListCellDomain = createFactory(require("./RequestListCellDomain"));
 const RequestListEmptyNotice = createFactory(require("./RequestListEmptyNotice"));
-const StatusBar = createFactory(require("./StatusBar"));
-
-const { div } = DOM;
+const RequestListHeader = createFactory(require("./RequestListHeader"));
 
 /**
  * Request panel component
  */
-function RequestList({
-  connector,
-  isEmpty,
-}) {
-  return (
-    div({ className: "request-list-container" },
-      isEmpty ? RequestListEmptyNotice({ connector }) : RequestListContent({ connector }),
-      StatusBar({ connector }),
-    )
-  );
+class RequestList extends Component {
+  static get propTypes() {
+    return {
+      connector: PropTypes.object.isRequired,
+      containerHeight: PropTypes.number.isRequired,
+      containerWidth: PropTypes.number.isRequired,
+      isEmpty: PropTypes.bool.isRequired,
+      requests: PropTypes.object.isRequired,
+    };
+  }
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      columnWidths: {
+        status: 100,
+        method: 100,
+        file: 300,
+        scheme: 100,
+        cause: 100,
+        type: 100,
+        domain: 150,
+      },
+    };
+
+    this.onColumnResizeEndCallback = this.onColumnResizeEndCallback.bind(this);
+  }
+
+  onColumnResizeEndCallback(newColumnWidth, columnKey) {
+    this.setState(({
+      columnWidths: Object.assign(this.state.columnWidths, {
+        [columnKey]: newColumnWidth,
+      })
+    }));
+  }
+
+  render() {
+    let {
+      connector,
+      containerHeight,
+      containerWidth,
+      isEmpty,
+      requests,
+    } = this.props;
+    let { columnWidths } = this.state;
+
+    return (
+      isEmpty ?
+        RequestListEmptyNotice({ connector })
+      :
+        Table({
+          headerHeight: 24,
+          height: containerHeight,
+          width: containerWidth,
+          rowHeight: 24,
+          rowsCount: requests.size,
+          onColumnResizeEndCallback: this.onColumnResizeEndCallback,
+        },
+          Column({
+            columnKey: "status",
+            header: RequestListHeader(),
+            cell: RequestListCellStatus({ requests }),
+            isResizable: true,
+            width: columnWidths.status,
+          }),
+          Column({
+            columnKey: "method",
+            header: RequestListHeader(),
+            cell: RequestListCellMethod({ requests }),
+            isResizable: true,
+            width: columnWidths.method,
+          }),
+          Column({
+            columnKey: "file",
+            header: RequestListHeader(),
+            cell: RequestListCellFile({ requests }),
+            isResizable: true,
+            width: columnWidths.file,
+          }),
+          Column({
+            columnKey: "scheme",
+            header: RequestListHeader(),
+            cell: RequestListCellScheme({ requests }),
+            isResizable: true,
+            width: columnWidths.scheme,
+          }),
+          Column({
+            columnKey: "cause",
+            header: RequestListHeader(),
+            cell: RequestListCellCause({ requests }),
+            isResizable: true,
+            width: columnWidths.cause,
+          }),
+          Column({
+            columnKey: "type",
+            header: RequestListHeader(),
+            cell: RequestListCellType({ requests }),
+            isResizable: true,
+            width: columnWidths.type,
+          }),
+          Column({
+            columnKey: "domain",
+            header: RequestListHeader(),
+            cell: RequestListCellDomain({ requests }),
+            flexGrow: 1,
+            isResizable: true,
+            width: columnWidths.domain,
+          }),
+        )
+    );
+  }
 }
 
-RequestList.displayName = "RequestList";
-
-RequestList.propTypes = {
-  connector: PropTypes.object.isRequired,
-  isEmpty: PropTypes.bool.isRequired,
-};
-
-module.exports = RequestList;
+module.exports = connect(
+  (state) => ({
+    columns: state.ui.columns,
+    isEmpty: state.requests.requests.isEmpty(),
+    requests: getDisplayedRequests(state),
+    firstRequestStartedMillis: state.requests.firstStartedMillis,
+    selectedRequestId: state.requests.selectedId,
+    scale: getWaterfallScale(state),
+  }),
+)(Dimensions()(RequestList));
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/RequestListCellCause.js
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  Component,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+
+const { div } = DOM;
+
+class RequestListColumnCause extends Component {
+  static get propTypes() {
+    return {
+      onCauseBadgeMouseDown: PropTypes.func.isRequired,
+      requests: PropTypes.object.isRequired,
+      rowIndex: PropTypes.number,
+    };
+  }
+
+  shouldComponentUpdate(nextProps) {
+    let { requests, rowIndex } = this.props;
+    return requests.get(rowIndex).cause !== nextProps.requests.get(rowIndex).cause;
+  }
+
+  render() {
+    let { onCauseBadgeMouseDown, requests, rowIndex } = this.props;
+    let { cause } = requests.get(rowIndex);
+
+    let causeType = "unknown";
+    let causeHasStack = false;
+
+    if (cause) {
+      // Legacy server might send a numeric value. Display it as "unknown"
+      causeType = typeof cause.type === "string" ? cause.type : "unknown";
+      causeHasStack = cause.stacktrace && cause.stacktrace.length > 0;
+    }
+
+    return (
+      div({ className: "requests-list-column requests-list-cause", title: causeType },
+        causeHasStack && div({
+          className: "requests-list-cause-stack",
+          onMouseDown: onCauseBadgeMouseDown,
+        }, "JS"),
+        causeType,
+      )
+    );
+  }
+}
+
+module.exports = RequestListColumnCause;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/RequestListCellDomain.js
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  Component,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { getFormattedIPAndPort } = require("../utils/format-utils");
+const { L10N } = require("../utils/l10n");
+const { propertiesEqual } = require("../utils/request-utils");
+
+const { div } = DOM;
+
+const UPDATED_DOMAIN_PROPS = [
+  "remoteAddress",
+  "securityState",
+  "urlDetails",
+];
+
+class RequestListColumnDomain extends Component {
+  static get propTypes() {
+    return {
+      requests: PropTypes.object.isRequired,
+      rowIndex: PropTypes.number,
+      onSecurityIconMouseDown: PropTypes.func.isRequired,
+    };
+  }
+
+  shouldComponentUpdate(nextProps) {
+    let { requests, rowIndex } = this.props;
+    return !propertiesEqual(UPDATED_DOMAIN_PROPS,
+      requests.get(rowIndex), nextProps.requests.get(rowIndex));
+  }
+
+  render() {
+    let { onSecurityIconMouseDown, requests, rowIndex } = this.props;
+    let {
+      remoteAddress,
+      remotePort,
+      securityState,
+      urlDetails: { host, isLocal },
+    } = requests.get(rowIndex);
+    let iconClassList = ["requests-security-state-icon"];
+    let iconTitle;
+    let title = host + (remoteAddress ?
+      ` (${getFormattedIPAndPort(remoteAddress, remotePort)})` : "");
+
+    if (isLocal) {
+      iconClassList.push("security-state-local");
+      iconTitle = L10N.getStr("netmonitor.security.state.secure");
+    } else if (securityState) {
+      iconClassList.push(`security-state-${securityState}`);
+      iconTitle = L10N.getStr(`netmonitor.security.state.${securityState}`);
+    }
+
+    return (
+      div({ className: "requests-list-column requests-list-domain", title },
+        div({
+          className: iconClassList.join(" "),
+          onMouseDown: onSecurityIconMouseDown,
+          title: iconTitle,
+        }),
+        host,
+      )
+    );
+  }
+}
+
+module.exports = RequestListColumnDomain;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/RequestListCellFile.js
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  Component,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { propertiesEqual } = require("../utils/request-utils");
+
+const { div, img } = DOM;
+
+const UPDATED_FILE_PROPS = [
+  "responseContentDataUri",
+  "urlDetails",
+];
+
+class RequestListColumnFile extends Component {
+  static get propTypes() {
+    return {
+      requests: PropTypes.object.isRequired,
+      rowIndex: PropTypes.number,
+      onThumbnailMouseDown: PropTypes.func.isRequired,
+    };
+  }
+
+  shouldComponentUpdate(nextProps) {
+    let { requests, rowIndex } = this.props;
+    return !propertiesEqual(UPDATED_FILE_PROPS,
+      requests.get(rowIndex), nextProps.requests.get(rowIndex));
+  }
+
+  render() {
+    let { onThumbnailMouseDown, requests, rowIndex } = this.props;
+    let { responseContentDataUri, urlDetails } = requests.get(rowIndex);
+
+    return (
+      div({
+        className: "requests-list-column requests-list-file",
+        title: urlDetails.unicodeUrl,
+      },
+        img({
+          className: "requests-list-icon",
+          src: responseContentDataUri,
+          onMouseDown: onThumbnailMouseDown,
+        }),
+        urlDetails.baseNameWithQuery
+      )
+    );
+  }
+}
+
+module.exports = RequestListColumnFile;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/RequestListCellMethod.js
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  Component,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+
+const { div } = DOM;
+
+class RequestListColumnMethod extends Component {
+  static get propTypes() {
+    return {
+      requests: PropTypes.object.isRequired,
+      rowIndex: PropTypes.number,
+    };
+  }
+
+  shouldComponentUpdate(nextProps) {
+    let { requests, rowIndex } = this.props;
+    return requests.get(rowIndex).method !== nextProps.requests.get(rowIndex).method;
+  }
+
+  render() {
+    let { requests, rowIndex } = this.props;
+    let { method } = requests.get(rowIndex);
+    return div({ className: "requests-list-column requests-list-method" }, method);
+  }
+}
+
+module.exports = RequestListColumnMethod;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/RequestListCellScheme.js
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  Component,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+
+const { div } = DOM;
+
+class RequestListColumnScheme extends Component {
+  static get propTypes() {
+    return {
+      requests: PropTypes.object.isRequired,
+      rowIndex: PropTypes.number,
+    };
+  }
+
+  shouldComponentUpdate(nextProps) {
+    let { requests, rowIndex } = this.props;
+    return requests.get(rowIndex).urlDetails.scheme !==
+      nextProps.requests.get(rowIndex).urlDetails.scheme;
+  }
+
+  render() {
+    let { requests, rowIndex } = this.props;
+    let { urlDetails } = requests.get(rowIndex);
+
+    return (
+      div({
+        className: "requests-list-column requests-list-scheme",
+        title: urlDetails.scheme,
+      },
+        urlDetails.scheme,
+      )
+    );
+  }
+}
+
+module.exports = RequestListColumnScheme;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/RequestListCellStatus.js
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  Component,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils/l10n");
+const { propertiesEqual } = require("../utils/request-utils");
+
+const { div } = DOM;
+
+const UPDATED_STATUS_PROPS = [
+  "fromCache",
+  "fromServiceWorker",
+  "status",
+  "statusText",
+];
+
+class RequestListColumnStatus extends Component {
+  static get propTypes() {
+    return {
+      requests: PropTypes.object.isRequired,
+      rowIndex: PropTypes.number,
+    };
+  }
+
+  constructor(props) {
+    super(props);
+    this.onMouseOver = this.onMouseOver.bind(this);
+  }
+
+  shouldComponentUpdate(nextProps) {
+    let { requests, rowIndex } = this.props;
+    return !propertiesEqual(UPDATED_STATUS_PROPS,
+      requests.get(rowIndex), nextProps.requests.get(rowIndex));
+  }
+
+  onMouseOver({ target }) {
+    let { requests, rowIndex } = this.props;
+    let { fromCache, fromServiceWorker, status, statusText } = requests.get(rowIndex);
+
+    if (status && statusText && !target.title) {
+      if (fromCache && fromServiceWorker) {
+        target.title = L10N.getFormatStr("netmonitor.status.tooltip.cachedworker",
+          status, statusText);
+      } else if (fromCache) {
+        target.title = L10N.getFormatStr("netmonitor.status.tooltip.cached",
+          status, statusText);
+      } else if (fromServiceWorker) {
+        target.title = L10N.getFormatStr("netmonitor.status.tooltip.worker",
+          status, statusText);
+      } else {
+        target.title = L10N.getFormatStr("netmonitor.status.tooltip.simple",
+          status, statusText);
+      }
+    }
+  }
+
+  render() {
+    let { requests, rowIndex } = this.props;
+    let { fromCache, fromServiceWorker, status } = requests.get(rowIndex);
+    let code;
+
+    if (status) {
+      if (fromCache) {
+        code = "cached";
+      } else if (fromServiceWorker) {
+        code = "service worker";
+      } else {
+        code = status;
+      }
+    }
+
+    return (
+      div({
+        className: "requests-list-column requests-list-status",
+        onMouseOver: this.onMouseOver,
+      },
+        div({ className: "requests-list-status-icon", "data-code": code }),
+        div({ className: "requests-list-status-code" }, status)
+      )
+    );
+  }
+}
+
+module.exports = RequestListColumnStatus;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/RequestListCellType.js
@@ -0,0 +1,49 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  Component,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { getAbbreviatedMimeType } = require("../utils/request-utils");
+
+const { div } = DOM;
+
+class RequestListColumnType extends Component {
+  static get propTypes() {
+    return {
+      requests: PropTypes.object.isRequired,
+      rowIndex: PropTypes.number,
+    };
+  }
+
+  shouldComponentUpdate(nextProps) {
+    let { requests, rowIndex } = this.props;
+    return requests.get(rowIndex).mimeType !== nextProps.requests.get(rowIndex).mimeType;
+  }
+
+  render() {
+    let { requests, rowIndex } = this.props;
+    let { mimeType } = requests.get(rowIndex);
+    let abbrevType;
+
+    if (mimeType) {
+      abbrevType = getAbbreviatedMimeType(mimeType);
+    }
+
+    return (
+      div({
+        className: "requests-list-column requests-list-type",
+        title: mimeType,
+      },
+        abbrevType
+      )
+    );
+  }
+}
+
+module.exports = RequestListColumnType;
deleted file mode 100644
--- a/devtools/client/netmonitor/src/components/RequestListColumnCause.js
+++ /dev/null
@@ -1,54 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const {
-  Component,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-
-const { div } = DOM;
-
-class RequestListColumnCause extends Component {
-  static get propTypes() {
-    return {
-      item: PropTypes.object.isRequired,
-      onCauseBadgeMouseDown: PropTypes.func.isRequired,
-    };
-  }
-
-  shouldComponentUpdate(nextProps) {
-    return this.props.item.cause !== nextProps.item.cause;
-  }
-
-  render() {
-    let {
-      item: { cause },
-      onCauseBadgeMouseDown,
-    } = this.props;
-
-    let causeType = "unknown";
-    let causeHasStack = false;
-
-    if (cause) {
-      // Legacy server might send a numeric value. Display it as "unknown"
-      causeType = typeof cause.type === "string" ? cause.type : "unknown";
-      causeHasStack = cause.stacktrace && cause.stacktrace.length > 0;
-    }
-
-    return (
-      div({ className: "requests-list-column requests-list-cause", title: causeType },
-        causeHasStack && div({
-          className: "requests-list-cause-stack",
-          onMouseDown: onCauseBadgeMouseDown,
-        }, "JS"),
-        causeType
-      )
-    );
-  }
-}
-
-module.exports = RequestListColumnCause;
deleted file mode 100644
--- a/devtools/client/netmonitor/src/components/RequestListColumnDomain.js
+++ /dev/null
@@ -1,66 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const {
-  Component,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { getFormattedIPAndPort } = require("../utils/format-utils");
-const { L10N } = require("../utils/l10n");
-const { propertiesEqual } = require("../utils/request-utils");
-
-const { div } = DOM;
-
-const UPDATED_DOMAIN_PROPS = [
-  "remoteAddress",
-  "securityState",
-  "urlDetails",
-];
-
-class RequestListColumnDomain extends Component {
-  static get propTypes() {
-    return {
-      item: PropTypes.object.isRequired,
-      onSecurityIconMouseDown: PropTypes.func.isRequired,
-    };
-  }
-
-  shouldComponentUpdate(nextProps) {
-    return !propertiesEqual(UPDATED_DOMAIN_PROPS, this.props.item, nextProps.item);
-  }
-
-  render() {
-    let { item, onSecurityIconMouseDown } = this.props;
-    let { remoteAddress, remotePort, securityState,
-      urlDetails: { host, isLocal } } = item;
-    let iconClassList = ["requests-security-state-icon"];
-    let iconTitle;
-    let title = host + (remoteAddress ?
-      ` (${getFormattedIPAndPort(remoteAddress, remotePort)})` : "");
-
-    if (isLocal) {
-      iconClassList.push("security-state-local");
-      iconTitle = L10N.getStr("netmonitor.security.state.secure");
-    } else if (securityState) {
-      iconClassList.push(`security-state-${securityState}`);
-      iconTitle = L10N.getStr(`netmonitor.security.state.${securityState}`);
-    }
-
-    return (
-      div({ className: "requests-list-column requests-list-domain", title },
-        div({
-          className: iconClassList.join(" "),
-          onMouseDown: onSecurityIconMouseDown,
-          title: iconTitle,
-        }),
-        host,
-      )
-    );
-  }
-}
-
-module.exports = RequestListColumnDomain;
deleted file mode 100644
--- a/devtools/client/netmonitor/src/components/RequestListColumnFile.js
+++ /dev/null
@@ -1,55 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const {
-  Component,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { propertiesEqual } = require("../utils/request-utils");
-
-const { div, img } = DOM;
-
-const UPDATED_FILE_PROPS = [
-  "responseContentDataUri",
-  "urlDetails",
-];
-
-class RequestListColumnFile extends Component {
-  static get propTypes() {
-    return {
-      item: PropTypes.object.isRequired,
-      onThumbnailMouseDown: PropTypes.func.isRequired,
-    };
-  }
-
-  shouldComponentUpdate(nextProps) {
-    return !propertiesEqual(UPDATED_FILE_PROPS, this.props.item, nextProps.item);
-  }
-
-  render() {
-    let {
-      item: { responseContentDataUri, urlDetails },
-      onThumbnailMouseDown
-    } = this.props;
-
-    return (
-      div({
-        className: "requests-list-column requests-list-file",
-        title: urlDetails.unicodeUrl,
-      },
-        img({
-          className: "requests-list-icon",
-          src: responseContentDataUri,
-          onMouseDown: onThumbnailMouseDown,
-        }),
-        urlDetails.baseNameWithQuery
-      )
-    );
-  }
-}
-
-module.exports = RequestListColumnFile;
deleted file mode 100644
--- a/devtools/client/netmonitor/src/components/RequestListColumnMethod.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const {
-  Component,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-
-const { div } = DOM;
-
-class RequestListColumnMethod extends Component {
-  static get propTypes() {
-    return {
-      item: PropTypes.object.isRequired,
-    };
-  }
-
-  shouldComponentUpdate(nextProps) {
-    return this.props.item.method !== nextProps.item.method;
-  }
-
-  render() {
-    let { method } = this.props.item;
-    return div({ className: "requests-list-column requests-list-method" }, method);
-  }
-}
-
-module.exports = RequestListColumnMethod;
deleted file mode 100644
--- a/devtools/client/netmonitor/src/components/RequestListColumnScheme.js
+++ /dev/null
@@ -1,39 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const {
-  Component,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-
-const { div } = DOM;
-
-class RequestListColumnScheme extends Component {
-  static get propTypes() {
-    return {
-      item: PropTypes.object.isRequired,
-    };
-  }
-
-  shouldComponentUpdate(nextProps) {
-    return this.props.item.urlDetails.scheme !== nextProps.item.urlDetails.scheme;
-  }
-
-  render() {
-    const { urlDetails } = this.props.item;
-    return (
-      div({
-        className: "requests-list-column requests-list-scheme",
-        title: urlDetails.scheme
-      },
-        urlDetails.scheme
-      )
-    );
-  }
-}
-
-module.exports = RequestListColumnScheme;
deleted file mode 100644
--- a/devtools/client/netmonitor/src/components/RequestListColumnStatus.js
+++ /dev/null
@@ -1,85 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const {
-  Component,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { L10N } = require("../utils/l10n");
-const { propertiesEqual } = require("../utils/request-utils");
-
-const { div } = DOM;
-
-const UPDATED_STATUS_PROPS = [
-  "fromCache",
-  "fromServiceWorker",
-  "status",
-  "statusText",
-];
-
-class RequestListColumnStatus extends Component {
-  static get propTypes() {
-    return {
-      item: PropTypes.object.isRequired,
-    };
-  }
-
-  shouldComponentUpdate(nextProps) {
-    return !propertiesEqual(UPDATED_STATUS_PROPS, this.props.item, nextProps.item);
-  }
-
-  render() {
-    let { item } = this.props;
-    let { fromCache, fromServiceWorker, status, statusText } = item;
-    let code;
-
-    if (status) {
-      if (fromCache) {
-        code = "cached";
-      } else if (fromServiceWorker) {
-        code = "service worker";
-      } else {
-        code = status;
-      }
-    }
-
-    return (
-      div({
-        className: "requests-list-column requests-list-status",
-        onMouseOver: function ({ target }) {
-          if (status && statusText && !target.title) {
-            target.title = getColumnTitle(item);
-          }
-        },
-      },
-      div({ className: "requests-list-status-icon", "data-code": code }),
-        div({ className: "requests-list-status-code" }, status)
-      )
-    );
-  }
-}
-
-function getColumnTitle(item) {
-  let { fromCache, fromServiceWorker, status, statusText } = item;
-  let title;
-  if (fromCache && fromServiceWorker) {
-    title = L10N.getFormatStr("netmonitor.status.tooltip.cachedworker",
-      status, statusText);
-  } else if (fromCache) {
-    title = L10N.getFormatStr("netmonitor.status.tooltip.cached",
-      status, statusText);
-  } else if (fromServiceWorker) {
-    title = L10N.getFormatStr("netmonitor.status.tooltip.worker",
-      status, statusText);
-  } else {
-    title = L10N.getFormatStr("netmonitor.status.tooltip.simple",
-      status, statusText);
-  }
-  return title;
-}
-
-module.exports = RequestListColumnStatus;
deleted file mode 100644
--- a/devtools/client/netmonitor/src/components/RequestListColumnType.js
+++ /dev/null
@@ -1,46 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const {
-  Component,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { getAbbreviatedMimeType } = require("../utils/request-utils");
-
-const { div } = DOM;
-
-class RequestListColumnType extends Component {
-  static get propTypes() {
-    return {
-      item: PropTypes.object.isRequired,
-    };
-  }
-
-  shouldComponentUpdate(nextProps) {
-    return this.props.item.mimeType !== nextProps.item.mimeType;
-  }
-
-  render() {
-    let { mimeType } = this.props.item;
-    let abbrevType;
-
-    if (mimeType) {
-      abbrevType = getAbbreviatedMimeType(mimeType);
-    }
-
-    return (
-      div({
-        className: "requests-list-column requests-list-type",
-        title: mimeType,
-      },
-        abbrevType
-      )
-    );
-  }
-}
-
-module.exports = RequestListColumnType;
--- a/devtools/client/netmonitor/src/components/RequestListEmptyNotice.js
+++ b/devtools/client/netmonitor/src/components/RequestListEmptyNotice.js
@@ -13,17 +13,16 @@ const {
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const Actions = require("../actions/index");
 const { ACTIVITY_TYPE } = require("../constants");
 const { L10N } = require("../utils/l10n");
 const { getPerformanceAnalysisURL } = require("../utils/mdn-utils");
 
 // Components
 const MDNLink = createFactory(require("./MdnLink"));
-const RequestListHeader = createFactory(require("./RequestListHeader"));
 
 const { button, div, span } = DOM;
 
 const RELOAD_NOTICE_1 = L10N.getStr("netmonitor.reloadNotice1");
 const RELOAD_NOTICE_2 = L10N.getStr("netmonitor.reloadNotice2");
 const RELOAD_NOTICE_3 = L10N.getStr("netmonitor.reloadNotice3");
 const PERFORMANCE_NOTICE_1 = L10N.getStr("netmonitor.perfNotice1");
 const PERFORMANCE_NOTICE_2 = L10N.getStr("netmonitor.perfNotice2");
@@ -38,43 +37,41 @@ class RequestListEmptyNotice extends Com
     return {
       connector: PropTypes.object.isRequired,
       onReloadClick: PropTypes.func.isRequired,
       onPerfClick: PropTypes.func.isRequired,
     };
   }
 
   render() {
-    return div(
-      {
-        className: "request-list-empty-notice",
-      },
-      RequestListHeader(),
-      div({ className: "notice-reload-message empty-notice-element" },
-        span(null, RELOAD_NOTICE_1),
-        button(
-          {
-            className: "devtools-button requests-list-reload-notice-button",
+    return (
+      div({ className: "request-list-empty-notice" },
+        div({ className: "notice-reload-message empty-notice-element" },
+          span(null, RELOAD_NOTICE_1),
+          button(
+            {
+              className: "devtools-button requests-list-reload-notice-button",
+              "data-standalone": true,
+              onClick: this.props.onReloadClick,
+            },
+            RELOAD_NOTICE_2
+          ),
+          span(null, RELOAD_NOTICE_3)
+        ),
+        div({ className: "notice-perf-message empty-notice-element" },
+          span(null, PERFORMANCE_NOTICE_1),
+          button({
+            title: PERFORMANCE_NOTICE_3,
+            className: "devtools-button requests-list-perf-notice-button",
             "data-standalone": true,
-            onClick: this.props.onReloadClick,
-          },
-          RELOAD_NOTICE_2
-        ),
-        span(null, RELOAD_NOTICE_3)
-      ),
-      div({ className: "notice-perf-message empty-notice-element" },
-        span(null, PERFORMANCE_NOTICE_1),
-        button({
-          title: PERFORMANCE_NOTICE_3,
-          className: "devtools-button requests-list-perf-notice-button",
-          "data-standalone": true,
-          onClick: this.props.onPerfClick,
-        }),
-        span(null, PERFORMANCE_NOTICE_2),
-        MDNLink({ url: getPerformanceAnalysisURL() })
+            onClick: this.props.onPerfClick,
+          }),
+          span(null, PERFORMANCE_NOTICE_2),
+          MDNLink({ url: getPerformanceAnalysisURL() })
+        )
       )
     );
   }
 }
 
 module.exports = connect(
   undefined,
   (dispatch, props) => ({
--- a/devtools/client/netmonitor/src/components/RequestListHeader.js
+++ b/devtools/client/netmonitor/src/components/RequestListHeader.js
@@ -1,233 +1,34 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {
-  Component,
+  PureComponent,
   PropTypes,
   DOM,
 } = require("devtools/client/shared/vendor/react");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-const { getTheme, addThemeObserver, removeThemeObserver } =
-  require("devtools/client/shared/theme");
-const Actions = require("../actions/index");
-const { HEADERS, REQUESTS_WATERFALL } = require("../constants");
-const { getWaterfallScale } = require("../selectors/index");
-const { getFormattedTime } = require("../utils/format-utils");
-const { L10N } = require("../utils/l10n");
-const WaterfallBackground = require("../waterfall-background");
-const RequestListHeaderContextMenu = require("../request-list-header-context-menu");
 
-const { div, button } = DOM;
+const { div } = DOM;
 
 /**
  * Render the request list header with sorting arrows for columns.
  * Displays tick marks in the waterfall column header.
  * Also draws the waterfall background canvas and updates it when needed.
  */
-class RequestListHeader extends Component {
+class RequestListHeader extends PureComponent {
   static get propTypes() {
     return {
-      columns: PropTypes.object.isRequired,
-      resetColumns: PropTypes.func.isRequired,
-      resizeWaterfall: PropTypes.func.isRequired,
-      scale: PropTypes.number,
-      sort: PropTypes.object,
-      sortBy: PropTypes.func.isRequired,
-      toggleColumn: PropTypes.func.isRequired,
-      waterfallWidth: PropTypes.number,
+      columnKey: PropTypes.string,
     };
   }
 
-  constructor(props) {
-    super(props);
-    this.onContextMenu = this.onContextMenu.bind(this);
-    this.drawBackground = this.drawBackground.bind(this);
-    this.resizeWaterfall = this.resizeWaterfall.bind(this);
-  }
-
-  componentWillMount() {
-    const { resetColumns, toggleColumn } = this.props;
-    this.contextMenu = new RequestListHeaderContextMenu({
-      resetColumns,
-      toggleColumn,
-    });
-  }
-
-  componentDidMount() {
-    // Create the object that takes care of drawing the waterfall canvas background
-    this.background = new WaterfallBackground(document);
-    this.drawBackground();
-    this.resizeWaterfall();
-    window.addEventListener("resize", this.resizeWaterfall);
-    addThemeObserver(this.drawBackground);
-  }
-
-  componentDidUpdate() {
-    this.drawBackground();
-  }
-
-  componentWillUnmount() {
-    this.background.destroy();
-    this.background = null;
-    window.removeEventListener("resize", this.resizeWaterfall);
-    removeThemeObserver(this.drawBackground);
-  }
-
-  onContextMenu(evt) {
-    evt.preventDefault();
-    this.contextMenu.open(evt);
-  }
-
-  drawBackground() {
-    // The background component is theme dependent, so add the current theme to the props.
-    let props = Object.assign({}, this.props, {
-      theme: getTheme()
-    });
-    this.background.draw(props);
-  }
-
-  resizeWaterfall() {
-    let waterfallHeader = this.refs.waterfallHeader;
-    if (waterfallHeader) {
-      // Measure its width and update the 'waterfallWidth' property in the store.
-      // The 'waterfallWidth' will be further updated on every window resize.
-      window.cancelIdleCallback(this._resizeTimerId);
-      this._resizeTimerId = window.requestIdleCallback(() =>
-        this.props.resizeWaterfall(waterfallHeader.getBoundingClientRect().width));
-    }
-  }
-
   render() {
-    let { columns, scale, sort, sortBy, waterfallWidth } = this.props;
-
-    return (
-      div({ className: "devtools-toolbar requests-list-headers-wrapper" },
-        div({ className: "devtools-toolbar requests-list-headers" },
-          HEADERS.filter((header) => columns.get(header.name)).map((header) => {
-            let name = header.name;
-            let boxName = header.boxName || name;
-            let label = header.noLocalization
-              ? name : L10N.getStr(`netmonitor.toolbar.${header.label || name}`);
-            let sorted, sortedTitle;
-            let active = sort.type == name ? true : undefined;
-
-            if (active) {
-              sorted = sort.ascending ? "ascending" : "descending";
-              sortedTitle = L10N.getStr(sort.ascending
-                ? "networkMenu.sortedAsc"
-                : "networkMenu.sortedDesc");
-            }
+    let { columnKey } = this.props;
 
-            return (
-              div({
-                id: `requests-list-${boxName}-header-box`,
-                className: `requests-list-column requests-list-${boxName}`,
-                key: name,
-                ref: `${name}Header`,
-                // Used to style the next column.
-                "data-active": active,
-                onContextMenu: this.onContextMenu,
-              },
-                button({
-                  id: `requests-list-${name}-button`,
-                  className: `requests-list-header-button`,
-                  "data-sorted": sorted,
-                  title: sortedTitle ? `${label} (${sortedTitle})` : label,
-                  onClick: () => sortBy(name),
-                },
-                  name === "waterfall"
-                    ? WaterfallLabel(waterfallWidth, scale, label)
-                    : div({ className: "button-text" }, label),
-                  div({ className: "button-icon" })
-                )
-              )
-            );
-          })
-        )
-      )
-    );
+    return div({ className: "requests-list-header" }, columnKey);
   }
 }
 
-/**
- * Build the waterfall header - timing tick marks with the right spacing
- */
-function waterfallDivisionLabels(waterfallWidth, scale) {
-  let labels = [];
-
-  // Build new millisecond tick labels...
-  let timingStep = REQUESTS_WATERFALL.HEADER_TICKS_MULTIPLE;
-  let scaledStep = scale * timingStep;
-
-  // Ignore any divisions that would end up being too close to each other.
-  while (scaledStep < REQUESTS_WATERFALL.HEADER_TICKS_SPACING_MIN) {
-    scaledStep *= 2;
-  }
-
-  // Insert one label for each division on the current scale.
-  for (let x = 0; x < waterfallWidth; x += scaledStep) {
-    let millisecondTime = x / scale;
-    let divisionScale = "millisecond";
-
-    // If the division is greater than 1 minute.
-    if (millisecondTime > 60000) {
-      divisionScale = "minute";
-    } else if (millisecondTime > 1000) {
-      // If the division is greater than 1 second.
-      divisionScale = "second";
-    }
-
-    let width = (x + scaledStep | 0) - (x | 0);
-    // Adjust the first marker for the borders
-    if (x == 0) {
-      width -= 2;
-    }
-    // Last marker doesn't need a width specified at all
-    if (x + scaledStep >= waterfallWidth) {
-      width = undefined;
-    }
-
-    labels.push(div(
-      {
-        key: labels.length,
-        className: "requests-list-timings-division",
-        "data-division-scale": divisionScale,
-        style: { width }
-      },
-      getFormattedTime(millisecondTime)
-    ));
-  }
-
-  return labels;
-}
-
-function WaterfallLabel(waterfallWidth, scale, label) {
-  let className = "button-text requests-list-waterfall-label-wrapper";
-
-  if (waterfallWidth !== null && scale !== null) {
-    label = waterfallDivisionLabels(waterfallWidth, scale);
-    className += " requests-list-waterfall-visible";
-  }
-
-  return div({ className }, label);
-}
-
-module.exports = connect(
-  (state) => ({
-    columns: state.ui.columns,
-    firstRequestStartedMillis: state.requests.firstStartedMillis,
-    scale: getWaterfallScale(state),
-    sort: state.sort,
-    timingMarkers: state.timingMarkers,
-    waterfallWidth: state.ui.waterfallWidth,
-  }),
-  (dispatch) => ({
-    resetColumns: () => dispatch(Actions.resetColumns()),
-    resizeWaterfall: (width) => dispatch(Actions.resizeWaterfall(width)),
-    sortBy: (type) => dispatch(Actions.sortBy(type)),
-    toggleColumn: (column) => dispatch(Actions.toggleColumn(column)),
-  })
-)(RequestListHeader);
+module.exports = RequestListHeader;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/RequestListHeaders.js
@@ -0,0 +1,233 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  Component,
+  PropTypes,
+  DOM,
+} = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { getTheme, addThemeObserver, removeThemeObserver } =
+  require("devtools/client/shared/theme");
+const Actions = require("../actions/index");
+const { HEADERS, REQUESTS_WATERFALL } = require("../constants");
+const { getWaterfallScale } = require("../selectors/index");
+const { getFormattedTime } = require("../utils/format-utils");
+const { L10N } = require("../utils/l10n");
+const WaterfallBackground = require("../waterfall-background");
+const RequestListHeaderContextMenu = require("../request-list-header-context-menu");
+
+const { div, button } = DOM;
+
+/**
+ * Render the request list header with sorting arrows for columns.
+ * Displays tick marks in the waterfall column header.
+ * Also draws the waterfall background canvas and updates it when needed.
+ */
+class RequestListHeader extends Component {
+  static get propTypes() {
+    return {
+      columns: PropTypes.object.isRequired,
+      resetColumns: PropTypes.func.isRequired,
+      resizeWaterfall: PropTypes.func.isRequired,
+      scale: PropTypes.number,
+      sort: PropTypes.object,
+      sortBy: PropTypes.func.isRequired,
+      toggleColumn: PropTypes.func.isRequired,
+      waterfallWidth: PropTypes.number,
+    };
+  }
+
+  constructor(props) {
+    super(props);
+    this.onContextMenu = this.onContextMenu.bind(this);
+    this.drawBackground = this.drawBackground.bind(this);
+    this.resizeWaterfall = this.resizeWaterfall.bind(this);
+  }
+
+  componentWillMount() {
+    const { resetColumns, toggleColumn } = this.props;
+    this.contextMenu = new RequestListHeaderContextMenu({
+      resetColumns,
+      toggleColumn,
+    });
+  }
+
+  componentDidMount() {
+    // Create the object that takes care of drawing the waterfall canvas background
+    this.background = new WaterfallBackground(document);
+    this.drawBackground();
+    this.resizeWaterfall();
+    window.addEventListener("resize", this.resizeWaterfall);
+    addThemeObserver(this.drawBackground);
+  }
+
+  componentDidUpdate() {
+    this.drawBackground();
+  }
+
+  componentWillUnmount() {
+    this.background.destroy();
+    this.background = null;
+    window.removeEventListener("resize", this.resizeWaterfall);
+    removeThemeObserver(this.drawBackground);
+  }
+
+  onContextMenu(evt) {
+    evt.preventDefault();
+    this.contextMenu.open(evt);
+  }
+
+  drawBackground() {
+    // The background component is theme dependent, so add the current theme to the props.
+    let props = Object.assign({}, this.props, {
+      theme: getTheme()
+    });
+    this.background.draw(props);
+  }
+
+  resizeWaterfall() {
+    let waterfallHeader = this.refs.waterfallHeader;
+    if (waterfallHeader) {
+      // Measure its width and update the 'waterfallWidth' property in the store.
+      // The 'waterfallWidth' will be further updated on every window resize.
+      window.cancelIdleCallback(this._resizeTimerId);
+      this._resizeTimerId = window.requestIdleCallback(() =>
+        this.props.resizeWaterfall(waterfallHeader.getBoundingClientRect().width));
+    }
+  }
+
+  render() {
+    let { columns, scale, sort, sortBy, waterfallWidth } = this.props;
+
+    return (
+      div({ className: "devtools-toolbar requests-list-headers-wrapper" },
+        div({ className: "devtools-toolbar requests-list-headers" },
+          HEADERS.filter((header) => columns.get(header.name)).map((header) => {
+            let name = header.name;
+            let boxName = header.boxName || name;
+            let label = header.noLocalization
+              ? name : L10N.getStr(`netmonitor.toolbar.${header.label || name}`);
+            let sorted, sortedTitle;
+            let active = sort.type == name ? true : undefined;
+
+            if (active) {
+              sorted = sort.ascending ? "ascending" : "descending";
+              sortedTitle = L10N.getStr(sort.ascending
+                ? "networkMenu.sortedAsc"
+                : "networkMenu.sortedDesc");
+            }
+
+            return (
+              div({
+                id: `requests-list-${boxName}-header-box`,
+                className: `requests-list-column requests-list-${boxName}`,
+                key: name,
+                ref: `${name}Header`,
+                // Used to style the next column.
+                "data-active": active,
+                onContextMenu: this.onContextMenu,
+              },
+                button({
+                  id: `requests-list-${name}-button`,
+                  className: `requests-list-header-button`,
+                  "data-sorted": sorted,
+                  title: sortedTitle ? `${label} (${sortedTitle})` : label,
+                  onClick: () => sortBy(name),
+                },
+                  name === "waterfall"
+                    ? WaterfallLabel(waterfallWidth, scale, label)
+                    : div({ className: "button-text" }, label),
+                  div({ className: "button-icon" })
+                )
+              )
+            );
+          })
+        )
+      )
+    );
+  }
+}
+
+/**
+ * Build the waterfall header - timing tick marks with the right spacing
+ */
+function waterfallDivisionLabels(waterfallWidth, scale) {
+  let labels = [];
+
+  // Build new millisecond tick labels...
+  let timingStep = REQUESTS_WATERFALL.HEADER_TICKS_MULTIPLE;
+  let scaledStep = scale * timingStep;
+
+  // Ignore any divisions that would end up being too close to each other.
+  while (scaledStep < REQUESTS_WATERFALL.HEADER_TICKS_SPACING_MIN) {
+    scaledStep *= 2;
+  }
+
+  // Insert one label for each division on the current scale.
+  for (let x = 0; x < waterfallWidth; x += scaledStep) {
+    let millisecondTime = x / scale;
+    let divisionScale = "millisecond";
+
+    // If the division is greater than 1 minute.
+    if (millisecondTime > 60000) {
+      divisionScale = "minute";
+    } else if (millisecondTime > 1000) {
+      // If the division is greater than 1 second.
+      divisionScale = "second";
+    }
+
+    let width = (x + scaledStep | 0) - (x | 0);
+    // Adjust the first marker for the borders
+    if (x == 0) {
+      width -= 2;
+    }
+    // Last marker doesn't need a width specified at all
+    if (x + scaledStep >= waterfallWidth) {
+      width = undefined;
+    }
+
+    labels.push(div(
+      {
+        key: labels.length,
+        className: "requests-list-timings-division",
+        "data-division-scale": divisionScale,
+        style: { width }
+      },
+      getFormattedTime(millisecondTime)
+    ));
+  }
+
+  return labels;
+}
+
+function WaterfallLabel(waterfallWidth, scale, label) {
+  let className = "button-text requests-list-waterfall-label-wrapper";
+
+  if (waterfallWidth !== null && scale !== null) {
+    label = waterfallDivisionLabels(waterfallWidth, scale);
+    className += " requests-list-waterfall-visible";
+  }
+
+  return div({ className }, label);
+}
+
+module.exports = connect(
+  (state) => ({
+    columns: state.ui.columns,
+    firstRequestStartedMillis: state.requests.firstStartedMillis,
+    scale: getWaterfallScale(state),
+    sort: state.sort,
+    timingMarkers: state.timingMarkers,
+    waterfallWidth: state.ui.waterfallWidth,
+  }),
+  (dispatch) => ({
+    resetColumns: () => dispatch(Actions.resetColumns()),
+    resizeWaterfall: (width) => dispatch(Actions.resizeWaterfall(width)),
+    sortBy: (type) => dispatch(Actions.sortBy(type)),
+    toggleColumn: (column) => dispatch(Actions.toggleColumn(column)),
+  })
+)(RequestListHeader);
--- a/devtools/client/netmonitor/src/middleware/batching.js
+++ b/devtools/client/netmonitor/src/middleware/batching.js
@@ -1,17 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { BATCH_ACTIONS, BATCH_ENABLE, BATCH_RESET } = require("../constants");
 
-const REQUESTS_REFRESH_RATE = 50; // ms
+const REQUESTS_REFRESH_RATE = 200; // ms
 
 /**
  * Middleware that watches for actions with a "batch = true" value in their meta field.
  * These actions are queued and dispatched as one batch after a timeout.
  * Special actions that are handled by this middleware:
  * - BATCH_ENABLE can be used to enable and disable the batching.
  * - BATCH_RESET discards the actions that are currently in the queue.
  */
--- a/devtools/client/netmonitor/yarn.lock
+++ b/devtools/client/netmonitor/yarn.lock
@@ -1279,17 +1279,17 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, 
   dependencies:
     cipher-base "^1.0.3"
     create-hash "^1.1.0"
     inherits "^2.0.1"
     ripemd160 "^2.0.0"
     safe-buffer "^5.0.1"
     sha.js "^2.4.8"
 
-create-react-class@^15.6.0:
+create-react-class@^15.5.2, create-react-class@^15.6.0:
   version "15.6.2"
   resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.2.tgz#cf1ed15f12aad7f14ef5f2dfe05e6c42f91ef02a"
   dependencies:
     fbjs "^0.8.9"
     loose-envify "^1.3.1"
     object-assign "^4.1.1"
 
 cross-spawn@^5.0.1:
@@ -1674,16 +1674,20 @@ ee-first@1.1.1:
 electron-to-chromium@^1.2.7:
   version "1.3.8"
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.8.tgz#b2c8a2c79bb89fbbfd3724d9555e15095b5f5fb6"
 
 electron-to-chromium@^1.3.18:
   version "1.3.21"
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.21.tgz#a967ebdcfe8ed0083fc244d1894022a8e8113ea2"
 
+element-resize-event@^2.0.4:
+  version "2.0.9"
+  resolved "https://registry.yarnpkg.com/element-resize-event/-/element-resize-event-2.0.9.tgz#2f5e1581a296eb5275210c141bc56342e218f876"
+
 elliptic@^6.0.0:
   version "6.4.0"
   resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
   dependencies:
     bn.js "^4.4.0"
     brorand "^1.0.1"
     hash.js "^1.0.0"
     hmac-drbg "^1.0.0"
@@ -2053,16 +2057,23 @@ find-up@^1.0.0:
     pinkie-promise "^2.0.0"
 
 find-up@^2.0.0, find-up@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
   dependencies:
     locate-path "^2.0.0"
 
+fixed-data-table-2@^0.8.5:
+  version "0.8.5"
+  resolved "https://registry.yarnpkg.com/fixed-data-table-2/-/fixed-data-table-2-0.8.5.tgz#8f21a26a011c50b7fa959c1748d4762b7a7d629b"
+  dependencies:
+    create-react-class "^15.5.2"
+    prop-types "^15.5.8"
+
 flatten@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
 
 for-in@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
 
@@ -4080,17 +4091,17 @@ process@^0.11.0:
   resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
 
 promise@^7.1.1:
   version "7.1.1"
   resolved "https://registry.yarnpkg.com/promise/-/promise-7.1.1.tgz#489654c692616b8aa55b0724fa809bb7db49c5bf"
   dependencies:
     asap "~2.0.3"
 
-prop-types@^15.5.10:
+prop-types@^15.5.10, prop-types@^15.5.8:
   version "15.6.0"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856"
   dependencies:
     fbjs "^0.8.16"
     loose-envify "^1.3.1"
     object-assign "^4.1.1"
 
 properties-parser@^0.3.1:
@@ -4194,16 +4205,22 @@ rc@^1.1.7:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95"
   dependencies:
     deep-extend "~0.4.0"
     ini "~1.3.0"
     minimist "^1.2.0"
     strip-json-comments "~2.0.1"
 
+react-dimensions@^1.3.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/react-dimensions/-/react-dimensions-1.3.1.tgz#89c29bcd48828a74faeb07da1e461e1a354ccc48"
+  dependencies:
+    element-resize-event "^2.0.4"
+
 react-dom@=15.6.1:
   version "15.6.1"
   resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.1.tgz#2cb0ed4191038e53c209eb3a79a23e2a4cf99470"
   dependencies:
     fbjs "^0.8.9"
     loose-envify "^1.1.0"
     object-assign "^4.1.0"
     prop-types "^15.5.10"
@@ -4433,19 +4450,19 @@ require-directory@^2.1.1:
 require-from-string@^1.1.0:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418"
 
 require-main-filename@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
 
-reselect@^2.5.4:
-  version "2.5.4"
-  resolved "https://registry.yarnpkg.com/reselect/-/reselect-2.5.4.tgz#b7d23fdf00b83fa7ad0279546f8dbbbd765c7047"
+reselect@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/reselect/-/reselect-3.0.1.tgz#efdaa98ea7451324d092b2b2163a6a1d7a9a2147"
 
 resolve@1.1.7:
   version "1.1.7"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
 
 resolve@^1.2.0:
   version "1.3.3"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.3.tgz#655907c3469a8680dc2de3a275a8fdd69691f0e5"