diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
deleted file mode 100644
index 4534425fe67945149d7481fc72be82caa720358d..0000000000000000000000000000000000000000
--- a/.github/workflows/ci.yml
+++ /dev/null
@@ -1,55 +0,0 @@
-name: ci
-on: [push, pull_request]
-
-jobs:
-  fmt:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v1
-      - uses: actions/cache@v1
-        with:
-          path: ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
-          restore-keys: |
-            ${{ runner.os }}-go-
-      - name: Run make fmt
-        uses: ./ci/image
-        with:
-          args: make fmt
-
-  lint:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v1
-      - uses: actions/cache@v1
-        with:
-          path: ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
-          restore-keys: |
-            ${{ runner.os }}-go-
-      - name: Run make lint
-        uses: ./ci/image
-        with:
-          args: make lint
-
-  test:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v1
-      - uses: actions/cache@v1
-        with:
-          path: ~/go/pkg/mod
-          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
-          restore-keys: |
-            ${{ runner.os }}-go-
-      - name: Run make test
-        uses: ./ci/image
-        with:
-          args: make test
-        env:
-          COVERALLS_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
-      - name: Upload coverage.html
-        uses: actions/upload-artifact@master
-        with:
-          name: coverage
-          path: ci/out/coverage.html
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000000000000000000000000000000000000..41d3c201468cbbaf1a0b50a8b93c74e10f0e77d8
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,40 @@
+language: go
+go: 1.x
+dist: bionic
+
+env:
+  global:
+    - SHFMT_URL=https://github.com/mvdan/sh/releases/download/v3.0.1/shfmt_v3.0.1_linux_amd64
+    - GOFLAGS="-mod=readonly"
+
+jobs:
+  include:
+    - name: Format
+      before_script:
+        - sudo apt-get install -y npm
+        - sudo npm install -g prettier
+        - sudo curl -L "$SHFMT_URL" > /usr/local/bin/shfmt && sudo chmod +x /usr/local/bin/shfmt
+        - go get golang.org/x/tools/cmd/stringer
+        - go get golang.org/x/tools/cmd/goimports
+      script: make -j16 fmt
+    - name: Lint
+      before_script:
+        - sudo apt-get install -y shellcheck
+        - go get golang.org/x/lint/golint
+      script: make -j16 lint
+    - name: Test
+      before_script:
+        - sudo apt-get install -y chromium-browser
+        - go get github.com/agnivade/wasmbrowsertest
+        - go get github.com/mattn/goveralls
+      script: make -j16 test
+
+addons:
+  apt:
+    update: true
+
+cache:
+  npm: true
+  directories:
+    - ~/.cache
+    - ~/gopath/pkg
diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile
deleted file mode 100644
index ed408edab60fec348794e6ccd01c867672fb39a5..0000000000000000000000000000000000000000
--- a/ci/image/Dockerfile
+++ /dev/null
@@ -1,18 +0,0 @@
-FROM golang:1
-
-RUN apt-get update
-RUN apt-get install -y chromium npm shellcheck
-
-ARG SHFMT_URL=https://github.com/mvdan/sh/releases/download/v3.0.1/shfmt_v3.0.1_linux_amd64
-RUN curl -L $SHFMT_URL > /usr/local/bin/shfmt && chmod +x /usr/local/bin/shfmt
-
-ENV GOFLAGS="-mod=readonly"
-ENV CI=true
-ENV MAKEFLAGS="--jobs=16 --output-sync=target"
-
-RUN npm install -g prettier
-RUN go get golang.org/x/tools/cmd/stringer
-RUN go get golang.org/x/tools/cmd/goimports
-RUN go get golang.org/x/lint/golint
-RUN go get github.com/agnivade/wasmbrowsertest
-RUN go get github.com/mattn/goveralls
diff --git a/close_notjs.go b/close_notjs.go
index c25b088f19bad5f6a25a5ea76f633470608eebd8..2537299560bd2f7560a112d8c86a67b639aecf61 100644
--- a/close_notjs.go
+++ b/close_notjs.go
@@ -86,11 +86,11 @@ func (c *Conn) waitCloseHandshake() error {
 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
 	defer cancel()
 
-	err := c.readMu.Lock(ctx)
+	err := c.readMu.lock(ctx)
 	if err != nil {
 		return err
 	}
-	defer c.readMu.Unlock()
+	defer c.readMu.unlock()
 
 	if c.readCloseFrameErr != nil {
 		return c.readCloseFrameErr
diff --git a/conn_notjs.go b/conn_notjs.go
index 7ee60fbc300ea249fdd30535e92093f5a52ebd71..2ec5f5bf9f2a7902b557a5901968b31557b4b205 100644
--- a/conn_notjs.go
+++ b/conn_notjs.go
@@ -139,16 +139,9 @@ func (c *Conn) close(err error) {
 	c.rwc.Close()
 
 	go func() {
-		if c.client {
-			c.writeFrameMu.Lock(context.Background())
-			putBufioWriter(c.bw)
-		}
 		c.msgWriterState.close()
 
 		c.msgReader.close()
-		if c.client {
-			putBufioReader(c.br)
-		}
 	}()
 }
 
@@ -237,7 +230,11 @@ func newMu(c *Conn) *mu {
 	}
 }
 
-func (m *mu) Lock(ctx context.Context) error {
+func (m *mu) forceLock() {
+	m.ch <- struct{}{}
+}
+
+func (m *mu) lock(ctx context.Context) error {
 	select {
 	case <-m.c.closed:
 		return m.c.closeErr
@@ -246,11 +243,19 @@ func (m *mu) Lock(ctx context.Context) error {
 		m.c.close(err)
 		return err
 	case m.ch <- struct{}{}:
+		// To make sure the connection is certainly alive.
+		// As it's possible the send on m.ch was selected
+		// the receive on closed.
+		select {
+		case <-m.c.closed:
+			return m.c.closeErr
+		default:
+		}
 		return nil
 	}
 }
 
-func (m *mu) Unlock() {
+func (m *mu) unlock() {
 	select {
 	case <-m.ch:
 	default:
diff --git a/conn_test.go b/conn_test.go
index 64e6736fd334174196c29df92e718d3f1d9ae1b7..535afe247e6b759fe117e94c8beead9a41a8888a 100644
--- a/conn_test.go
+++ b/conn_test.go
@@ -268,6 +268,10 @@ func TestConn(t *testing.T) {
 func TestWasm(t *testing.T) {
 	t.Parallel()
 
+	if os.Getenv("CI") != "" {
+		t.Skip("skipping on CI")
+	}
+
 	var wg sync.WaitGroup
 	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		wg.Add(1)
diff --git a/dial.go b/dial.go
index f882f122f5578333d17d7f52390e5c529b6372a0..50a0ecce3f1aeedde43715337e2b67f85f50eb85 100644
--- a/dial.go
+++ b/dial.go
@@ -253,10 +253,10 @@ func verifyServerExtensions(copts *compressionOptions, h http.Header) (*compress
 	return copts, nil
 }
 
-var readerPool sync.Pool
+var bufioReaderPool sync.Pool
 
 func getBufioReader(r io.Reader) *bufio.Reader {
-	br, ok := readerPool.Get().(*bufio.Reader)
+	br, ok := bufioReaderPool.Get().(*bufio.Reader)
 	if !ok {
 		return bufio.NewReader(r)
 	}
@@ -265,13 +265,13 @@ func getBufioReader(r io.Reader) *bufio.Reader {
 }
 
 func putBufioReader(br *bufio.Reader) {
-	readerPool.Put(br)
+	bufioReaderPool.Put(br)
 }
 
-var writerPool sync.Pool
+var bufioWriterPool sync.Pool
 
 func getBufioWriter(w io.Writer) *bufio.Writer {
-	bw, ok := writerPool.Get().(*bufio.Writer)
+	bw, ok := bufioWriterPool.Get().(*bufio.Writer)
 	if !ok {
 		return bufio.NewWriter(w)
 	}
@@ -280,5 +280,5 @@ func getBufioWriter(w io.Writer) *bufio.Writer {
 }
 
 func putBufioWriter(bw *bufio.Writer) {
-	writerPool.Put(bw)
+	bufioWriterPool.Put(bw)
 }
diff --git a/read.go b/read.go
index a1efecabb269c1efceacab1557962cebb54841fa..f2c7a801984fb590c342cf1f2aa30bbc23ceddf5 100644
--- a/read.go
+++ b/read.go
@@ -109,12 +109,17 @@ func (mr *msgReader) putFlateReader() {
 }
 
 func (mr *msgReader) close() {
-	mr.c.readMu.Lock(context.Background())
+	mr.c.readMu.forceLock()
 	mr.putFlateReader()
 	mr.dict.close()
 	if mr.flateBufio != nil {
 		putBufioReader(mr.flateBufio)
 	}
+
+	if mr.c.client {
+		putBufioReader(mr.c.br)
+		mr.c.br = nil
+	}
 }
 
 func (mr *msgReader) flateContextTakeover() bool {
@@ -292,11 +297,11 @@ func (c *Conn) handleControl(ctx context.Context, h header) (err error) {
 func (c *Conn) reader(ctx context.Context) (_ MessageType, _ io.Reader, err error) {
 	defer errd.Wrap(&err, "failed to get reader")
 
-	err = c.readMu.Lock(ctx)
+	err = c.readMu.lock(ctx)
 	if err != nil {
 		return 0, nil, err
 	}
-	defer c.readMu.Unlock()
+	defer c.readMu.unlock()
 
 	if !c.msgReader.fin {
 		return 0, nil, errors.New("previous message not read to completion")
@@ -368,11 +373,11 @@ func (mr *msgReader) Read(p []byte) (n int, err error) {
 		errd.Wrap(&err, "failed to read")
 	}()
 
-	err = mr.c.readMu.Lock(mr.ctx)
+	err = mr.c.readMu.lock(mr.ctx)
 	if err != nil {
 		return 0, err
 	}
-	defer mr.c.readMu.Unlock()
+	defer mr.c.readMu.unlock()
 
 	n, err = mr.limitReader.Read(p)
 	if mr.flate && mr.flateContextTakeover() {
diff --git a/write.go b/write.go
index 81b9141ae7e06aac325de7868ed4302bdfef4975..2d20b292f4f1110b8434da672b0dcd8ff2a1e4c2 100644
--- a/write.go
+++ b/write.go
@@ -125,7 +125,7 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error
 	}
 
 	if !c.flate() {
-		defer c.msgWriterState.mu.Unlock()
+		defer c.msgWriterState.mu.unlock()
 		return c.writeFrame(ctx, true, false, c.msgWriterState.opcode, p)
 	}
 
@@ -139,7 +139,7 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error
 }
 
 func (mw *msgWriterState) reset(ctx context.Context, typ MessageType) error {
-	err := mw.mu.Lock(ctx)
+	err := mw.mu.lock(ctx)
 	if err != nil {
 		return err
 	}
@@ -204,11 +204,16 @@ func (mw *msgWriterState) Close() (err error) {
 	if mw.flate && !mw.flateContextTakeover() {
 		mw.dict.close()
 	}
-	mw.mu.Unlock()
+	mw.mu.unlock()
 	return nil
 }
 
 func (mw *msgWriterState) close() {
+	if mw.c.client {
+		mw.c.writeFrameMu.forceLock()
+		putBufioWriter(mw.c.bw)
+	}
+
 	mw.writeMu.Lock()
 	mw.dict.close()
 }
@@ -226,11 +231,11 @@ func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error
 
 // frame handles all writes to the connection.
 func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opcode, p []byte) (int, error) {
-	err := c.writeFrameMu.Lock(ctx)
+	err := c.writeFrameMu.lock(ctx)
 	if err != nil {
 		return 0, err
 	}
-	defer c.writeFrameMu.Unlock()
+	defer c.writeFrameMu.unlock()
 
 	select {
 	case <-c.closed: